Partnerzy
PolProg
Lomsel
KonradVme

Serwer sponsoruje

Certyfikaty

Valid HTML 4.01!
Valid CSS!

Cyfrowe przetwarzanie tekstu

Spis treści

  1. Wstęp
  2. ASCII Art
  3. Gry tekstowe
  4. Język naturalny
  5. Wyszukiwanie
  6. Języki opisu
  7. Języki programowania
  8. Zakończenie

Wstęp

Choć podobno obraz mówi więcej, niż tysiąc słów, nigdy nie uda mu się całkowicie wyprzeć tekstu z naszego życia. Nawet w dobie telewizji i multimediów czytanie książek pozostaje zajęciem ciekawym i potrzebnym, a wszelkie kolorowe magazyny czy nawet reklamy są tylko atrakcyjną wizualnie otoczką dla informacji niesionych przez litery i cyfry.

Tekst a komputer

Także w dziedzinie techniki cyfrowej tekst odgrywa wielką rolę. Można powiedzieć, że jest on najdoskonalszą formą komunikacji człowieka z maszyną. Oto schematyczne przedstawienie ich wzajemnych relacji:

Podobnie można przedstawić kilkupoziomowy proces programowania komputera:

Zastosowania tekstu

Jako sposób przekazywania informacji
Napisany przez jednego dokument może dla wielu stanowić źródło wiedzy i informacji, np. książka, artykuł czy strona WWW.
Jako forma komunikacji
Za pomocą tekstu można się porozumiewać tam, gdzie niemożliwe jest to bezpośrednio czy za pomocą głosu, np. poczta tradycyjna i elektroniczna, IRC.
Jako język opisu
W niektórych zastosowaniach dane używane przez programy wygodnie jest tworzyć i przechowywać w specjalnie opracowanych formatach tekstowych, np. XML.
Jako język programowania
Język (czyli zbiór reguł składniowych i semantycznych) wystarczająco podobny do języka naturalnego, by człowiek mógł po jego opanowaniu sprawnie formułować w nim swoje pomysły, a zarazem dostatecznie ścisły, by mógł być w sposób automatyczny przetwarzany przez komputer, to najlepsza metoda programowania.

Co można robić z tekstem?

Przechowywać i przesyłać
Jak każde dane
Wprowadzać i wyprowadzać
Pobierać od użytkownika i pokazywać użytkownikowi
Generować
Tworzyć za pomocą specjalnych algorytmów
Przeszukiwać
Sprawdzać obecność pewnych wyrazów
Parsować
Analizować wg składni języka, w którym jest zapisany
Wykonywać
Jeśli jest kodem w jakimś języku programowania

Wstęp właściwy :)

Po powyższym wstępie zapraszam na podróż do fascynującego świata tekstu i metod jego przetwarzania za pomocą komputera. Choć może na pozór nie jest on tak atrakcyjny, jak świat programowanie grafiki czy nawet dźwięku, to jednak jest nie mniej od nich ważny i zapewniam, że przy odpowiednim podejściu równie ciekawy.

Artykuł ten przeznaczony jest dla osób zajmujących się programowaniem. Jednak nie znajdzie się tu zbyt wiele kodów źródłowych. Tym bardziej nie jest jego celem nauka programowania. Opisane zostaną w sposób abstrakcyjny różne pojęcia i od Twojego stopnia zaawansowania zależy, czy jesteś je w stanie zaimplementować.

Ten artykuł to efekt mojego zainteresowania wszystkim, co tekstowe w komputerze i w programowaniu. Stanowi próbę przeglądu różnych zagadnień, a wiele z nich opisane jest bardzo skrótowo. W końcu nie na wszystkim, o czym tutaj napisałem, znam się dobrze i gdzieniegdzie chciałem tylko zasygnalizować pewne rzeczy zachęcając Cię to samodzielnego ich zgłębiania.

Omawiane tematy ułożone są w kolejności od najprostszych (nie od razu nawet bezpośrednio związanych z programowaniem) do coraz bardziej zaawansowanych. Na końcu wielu podrozdziałów znajduje się zbiór odsyłaczy do ciekawych stron WWW.

Problem kodowania

Początkowo planowałem umieścić tutaj wstęp do programowania tekstu wraz z opisem używanych do tego celu typów danych w języku C++ itp. Choć ostatecznie postanowiłem pozostawić wszelkie sprawy czysto programistyczne poza zakresem tego artykułu, to przed rozpoczęciem właściwego artykułu nie mogę nie wspomnieć o istotnym problemie, który pojawia się w związku z koniecznością kodowania polskich liter.

ASCII

W celu zakodowania tekstu przyporządkowano każdej liczbie (kombinacji bitów) pewien znak. Tak powstał kod ASCII. Do zakodowania jednego znaku używa on jednego bajta (8 bitów). Dostępnych jest więc 256 różnych znaków. Standard ten obowiązuje już od czasów systemu DOS aż po dziś dzień.

Tak naprawdę, sam standard ASCII wykorzystuje 7 bitów. Oznacza to, że dostępnych jest 128 różnych kombinacji bitów, czyli można zapisać 128 różnych znaków. Taka ilość wystarczyła, by pierwszym 32 znakom (odpowiadającym zakodowanym w systemie binarnym liczbom naturalnym 0...31) przypisać pewne specjalne kody sterujące, a dalej zmieścić cyfry, małe i duże litery alfabetu łacińskiego oraz wszystkie znaki znajdujące się na klawiaturze.

256 możliwych kombinacji bitów w jednym bajcie to jednak za mało, by zapisać znaki specyficzne dla różnych języków świata, jak polskie "ąćęłńóśżź" czy niemieckie "umlauty" (nie mówiąc już o zupełnie innych alfabetach, jak cyrylica czy znaki chińskie).

Dlatego dodatkowe 128 znaków powstałe po użyciu ósmego bitu nie jest ujednolicone. Stworzonych zostało wiele tzw. stron kodowych (ang. "codepage") używających tych dodatkowych znaków do kodowania liter alfabetów narodowych, a przy tym różnych symboli graficznych i innych bardziej lub mniej przydatnych. Jest z tym niestety dużo problemów. Nawet dla samego języka polskiego powstało kilka kodów.

W wielu zastosowaniach, szczególnie w Internecie (WWW, e-mail) w nagłówku zapisywana jest nazwa standardu kodowania użyty w danym dokumencie. Pozwala to zmniejszyć problemy wynikające z całego tego bałaganu.

Unikod

Rozwój Internetu stworzył konieczność wynalezienia lepszego sposobu kodowania znaków, niż wysłużony już kod ASCII. Nawet ze swoimi stronami kodowymi ten ostatni ma wiele ograniczeń i sprawia wiele problemów. Nie można chociażby zapisać tekstu w kilku różnych językach w jednym dokumencie.

Pomyślano więc tak: Właściwie, skoro dzisiejsze dyski mają pojemności mierzone w gigabajtach, a obrazki i filmy zajmują o całe rzędy wielkości więcej miejsca niż tekst, po co nadal ograniczać się do jednego bajta na znak? Dlaczego nie utworzyć kodu, w którym jeden znak zajmowałby, powiedzmy, 2 bajty?

Tak powstał Unikod (ang. "Unicode", w skrócie UCS). Warto zdać sobie sprawę z faktu, że już za pomocą 2 bajtów można zakodować 2162 = 65536 różnych znaków! Dlatego w unikodzie znalazło się miejsce dla wszelkich użytecznych i używanych na świecie liter, symboli i znaków, a po upowszechnieniu się tego standardu nasze dzieci będą już tylko od nas słyszały historie, jakie to kiedyś były problemy w komputerze z kodowaniem znaków :)

Najpopularniejszymi odmianami unikodu są UTF-8 i UTF-16. W tej pierwszej znak może mieć różną długość. Pierwsze 128 znaków pokrywa się z tablicą ASCII i jest zapisywana za pomocą jednego bajta, natomiast znaki dodatkowe (np. polskie literki) są zapisywane za pomocą specjalnych kilkubajtowych sekwencji. Z kolei UTF-16 określa standard, w którym każdy znak zajmuje 2 bajty.

Polskie litery

Oto tabela kodów polskich liter w ważniejszych stronach kodowych (cytowana za Polską Stroną Ogonkową):

                (a)    (c)    (e)    (l)    (n)    (o)    (s)    (x)    (z)
                 ą      ć      ę      ł      ń      ó      ś      ź      ż
---------------------------------------------------------------------------
ISO-8859-2      177    230    234    179    241    243    182    188    191
Windows-1250    185    230    234    179    241    243    156    159    191
IBM (CP852)     165    134    169    136    228    162    152    171    190
brak             97     99    101    108    110    111    115    122    122
Unicode      0x0105 0x0107 0x0119 0x0142 0x0144 0x00F3 0x015B 0x017A 0x017C

                (A)    (C)    (E)    (L)    (N)    (O)    (S)    (X)    (Z)
                 Ą      Ć      Ę      Ł      Ń      Ó      Ś      Ź      Ż
---------------------------------------------------------------------------
ISO-8859-2      161    198    202    163    209    211    166    172    175
Windows-1250    165    198    202    163    209    211    140    143    175
IBM (CP852)     164    143    168    157    227    224    151    141    189
brak             65     67     69     76     78     79     83     90     90
Unicode      0x0104 0x0106 0x0118 0x0141 0x0143 0x00D3 0x015A 0x0179 0x017B
ISO-8859-2 (Latin-2)
To oficjalny standard kodowania ustalony jako norma, zalecany do używania w Internecie i używany w systemie Linux.
Windows-1250
Przez wielu potępiany za to, że został wylansowany przez Microsoft. W praktyce jednak to właśnie jego używa system Windows, a wielu miejscach Internetu jest on nie mniej popularny, niż ten pierwszy.
IBM (CP852)
Standard używany w systemie DOS, a obecnie na konsoli Windows.
brak
Kody liter standardowych odpowiadających danym polskim literom.
Unicode
Dwubajtowe kody polskich liter w unikodzie zapisane w systemie szesnastkowym.

Aby sprawdzić, czy prawidłowo działają w jakimś programie, systemie czy gdziekolwiek indziej polskie litery, wpisuje się zazwyczaj utarty tekst: "Zażółć gęślą jaźń". Choć nie ma on większego sensu, ma to do siebie, że będąc poprawnym gramatycznie zdaniem zawiera w sobie wszystkie polskie literki.

Inne znaki specjalne

Dodatkowo standard Windows-1250 przewiduje kilka znaków specjalnych, które podczas konwersji na inne strony kodowe także trzeba zamieniać:

  • Cudzysłów dolny (132 = 0x84) - zamieniać na zwykły cudzysłów "
  • Cudzysłów górny (148 = 0x94) - zamieniać na zwykły cudzysłów "
  • Odwrócony cudzysłów górny (147 = 0x93) - zamieniać na zwykły cudzysłów "
  • Wielokropek (133 = 0x85) - zamieniać na trzy kropki "..."
  • Krótszy myślnik (150 = 0x96) - zamieniać na zwykły myślnik "-"
  • Dłuższy myślnik (151 = 0x97) - zamieniać na zwykły myślnik "-"
  • Apostrof (146 = 0x92) - zamieniać na zwykły apostrof '

Białe znaki

Pośród znaków ASCII wydzieloną grupę stanowią tzw. białe znaki (ang. "whitespace") albo inaczej odstępy. Są one uznawane za znaki oddzielające pewne części tekstu. Należą do nich 4 znaki:

  1. 0x09 (9) "\t" HT - tabulacja
  2. 0x0A (10) "\n" LF - koniec wiersza
  3. 0x0D (13) "\r" CR - powrót karetki
  4. 0x20 (32) " " - spacja

W wielu językach (w tym w językach programowania, np. C++ czy Pascal oraz w językach opisu, np. HTML czy XML) fakt, jakie spośród tych znaków wystąpią, w jakiej ilości i w jakiej kolejności, nie ma znaczenia - każda taka sekwencja traktowana jest jako pojedynczy odstęp. To dzięki temu możemy robić wcięcia w kodzie i swobodnie go rozmieszczać (zauważ, że wcięcie to znak końca wiersza plus pewna liczba spacji lub tabulacji). Takie traktowanie odstępów w jakimś języku nosi nazwę wolnego formatu zapisu.

Znaki końca wiersza

Poświęcenia dodatkowej uwagi wymaga temat znaków uznawanych za koniec wiersza (linii) w tekście. Panują w tej kwestii dwa różne standardy.

  • W Windows koniec wiersza zaznacza się sekwencją dwóch znaków: CR i LF
  • W Linux samym LF
  • W Mac samym CR

Z kompatybilnością między tymi formatami bywa różnie. W Linux podwójny koniec wiersza najczęściej zinterpretowany zostanie prawidłowo, o ile wiersz może kończyć się odstępem i ten odstęp zostanie zignorowany.

Notatnik Windows nie odczyta poprawnie dokumentu zapisanego ze znakami końca wiersza w stylu linuksowym. Na prawidłowe jego wyświetlenie możesz za to liczyć w programie Lister wbudowanym w Total Commander.

Aby edytować i zapisywać pod Windows dokumenty ze znakami końca wiersza w stylu linuksowym, możesz użyć jednego z tekstowych edytorów HTML, np. HomeSite lub Pajączek. Trzeba tylko uaktywnić specjalną opcję w konfiguracji. Nazywa się ona najczęściej zapisywaniem znaków końca wiersza w stylu Unix.

Linki

Polska Strona Ogonkowa
Serwis poświęcony problemom kodowania polskich znaków
ASCII Table
Tablica znaków ASCII
Unicode Home Page
Oficjalna strona standardu Unicode

ASCII Art

ASCII Art to sztuka tworzenia grafiki za pomocą odpowiednio ułożonych znaków tekstowych. Jej zwolennicy twierdzą, że to najbardziej naturalna dla komputera i najbardziej przenośna forma grafiki (działa wszędzie). Jej tworzenia można się prosto nauczyć, chociaż nie obejdzie się bez odrobiny talentu graficznego.

Wbrew pozorom, zajmowanie się tym niszowym działem grafiki nadal ma sens. Czysty, niesformatowany tekst (czyli niemogący zawierać liter różnych krojów, kolorów czy wielkości) nadal spotykamy w wielu miejscach, np. w e-mailach, IRC, grupach dyskusyjnych oraz w plikach typu "Readme" dołączanych do większości programów i zapisanych w nieśmiertelnym formacie TXT.

Za pomocą ASCII Art można rysować schematy, tabele, robić obramowania i różne ozdoby wokół tekstu, a także po prostu tworzyć rysunki. Do grafiki ASCII najczęściej używa się tekstu w jednym kolorze pisanego czcionką nieproporcjonalną (czyli taką, w której każdy znak, zarówno "m" jak i "l", mają taką samą szerokość), np. Courier New czy Fixedsys.

Dawniej wykorzystywano w tym celu pełną gamę 256 znaków ASCII razem z różnymi symbolami i "płotkami", których wygląd zależy od użytej strony kodowej. Takie, nie zawsze wyświetlane poprawnie pliki można dziś spotkać np. w crackach do różnych programów. Aby poprawnie obejrzeć taki plik, należy otworzyć go w programie Total Commander klawiszem [F3] (podglądnąć za pomocą Listera) i nacisnąć [S] (tryb "ASCII (DOS Charset)"). Do tworzenia ASCII Art najlepiej używać tylko standardowych liter i innych znaków dostępnych bezpośrednio na klawiaturze.

ASCIIzacja

:,:;::.o.;:,3:,::.;,,:,,7,:::::::::, MANNMMMMMMMMMMMMM@RM :::;;;;;;;;;77o7M
;..;., 2.7;.u:,::,3ooo3o3:::::::::,. MRM             3MMM :;;::;:;;;;;;;;,3
:. ;., 3 7: Z;.,... .   .........,,. MMM  ;7o;:obR;7o o8M ,,:::;;;;;;;;;;:o
;,.:   7 :, 3:,;;;7777oooooo7oo7777;.,  ,7722RMMMM,2MM:.Mu8oo7;;;;;;;;;;7:7
;..  ::3o3oo333333oooo7777;;;;;;:,..  3bNA8.  ,  3     .     .;::;;;;;;;;;7
;. ,M2oo7oo;;;;;;;;;;;::::;,,....;2EM@8;         :38bAu;,7.,.   .:;;;;;;;;;
;  bR27;,:;;;;7;;;;;;;;:::7o28RMMMA,   .  :2 ,3Z888b8bRZEEE@MHNR  :;;;;;;;;
7  8Uu7;::;77777;;7o28AEMMMH@R87      .  Z32NMU377ooo72U7AHH8,,RM..;;;;;;;;
7  .@u7;:::;;o2uZUHNR83;           ..   bZ8U3   ..;ZRU8ZUo.HU3RRb3 ,;;;;;;;
7.  MZ2o;7;;:;7@Z2   .,.   ..    .    MA28b:..;uAU8u23o773o2bbu33bu :;;;;;;
o . Rb3oooo3322bo .;;        ...    @M2bu3  288RUZ3;::,.,,,,,;2REAA :;;;;;;
o .  NZ8883:.     .    ..         MMER8b7:733R@827;;;;;;:. .,,,.uAM;;;;;;:;
3    M8;       .. ...     ..    EM@AAR8ou,72MU2337:  .,;;::::;o; o ;.:;;;;;
3    .      .         . .     AMMHRAbA;8,7.E;:3Z8u:ou8. .,,,,:;,8N o;:::::;
2   ,3..,::,:7;:,,          HMMH@@RARuAo;288oo;;ZMMM8MNR2;      MU ;;
2    2Z;,.    . ,: 2RMZ    MMHER@@Rb8AR.o2M223;, .2;.732;;MMMMH8M  ;.MMNMNE
2    Zo.,,,.     ,3EUHMNM3 M@EHHRuRU882 7;M3ZZ3;:    ,77 ;RU.8oMM :; uUbAA8
2   , Z2::,   ;uuo:,;;3;MM3bNHEAZ83bAZu,;u@7uZ237;;723oo 7    oH2 ;7,.RZZUU
3.  : 3M7.  3Z2o7;;:,:8MHHM ENZZu78H22o;:;R7o2333;.7bAR3 u7  2M;;::;; RuUUo
3; ,;.  ,uMAZoo7777;,,7@M@@M M2uZ2Moo;823;@:3272ZHAu7,.,:Uo:@Ho77;;:; MMMMM
o;  ::, 7 Hb237777o7:,;8ZEME.NU82N3,R M287URo37oo2AUZUMU3:bMUo:;7;:;;:,
77 ,.:: 7  NZ2777oo7:.;ZuZRHMHH ER U ubbH:ZRbZ2;.  ;oo , M@Zb3;;;;:,;77.,,:
77.,,:, ;; AAu37;7o7;,7ZZ2uUHHM.Mo,b RAbM 8R@RRAbu27: RMbHAHH2:,:;:::;u:;;;
77..::;,;o  Hboo;7777;72u332Z@M8M.8: HHU8o@EEAUb8233U33,MM83u3;::::::,3
77:::;:.:7,..Hb237;:;;7oZ23oouAMN b;8MH8uZ3o7;:,.,722@AMA2o77:,,;,::;:.H@@E
77,::;::,3,;  N8233oo777uuu23;;ZR:8u@RR@b;::7777o223EEuUo7;7o;:::::,:: NMHE
7o:;:;;:,3:;; ;A228u;;o;o32uu8Zu88o28uo;;;o32322u38bZ377;77;7::;;;;:,, @HRR
7o:;;;:;,o:;;.3 EZ2u2;77o237o322u283uo77ooo3333o3o377;7;;;;77.,::;;::. MERR
77:,;;::.o:;;,2 7b223;oo327;7;;;;;;777o7oo7o7o7777o77777;;;7o:::::::. UMERR
77;;;;;;;o;;7;u; b23oo332u33333o77777777737777ooo7ooooooo777o::;:,:;:3M@RRR

To jest przykład zdjęcia skonwertowanego automatyczne do postaci tekstowej w programie ASCII Generator. ASCIIzacja to proces przetwarzania zwykłego obrazu na ASCII Art poprzez pewne przekształcenie, które każdej grupie pikseli obrazka przyporządkowuje jakiś znak.

Można to zrealizować w taki sposób:

  1. Wstępnie przetworzyć obrazek - okroić, zmienić rozmiar (przeskalować), zmienić jasność czy kontrast
  2. Przetworzyć obrazek na postać lepiej nadającą się do ASCIIzacji - rozmyć lub wyostrzyć, wykryć krawędzie czy zastosować dowolne inne efekty (filtry)
  3. Zamienić go na postać pośrednią - np. z kolorów RGB na skalę szarości wg wzoru (ten wzór uwzględnia czułość ludzkiego oka na poszczególne składowe):
    I = 0.3*R + 0.59*G + 0.11*B
  4. Każdej prostokątnej grupie pikseli przyporządkować jakiś znak

Tą ostatnią czynność można wykonać na wiele sposobów. Najprostszym jest skonstruowanie ciągu znaków uporządkowanych według "stopnia zaciemnienia" i wybieranie ich na podstawie średniej jasności danego prostokąta na obrazku. Takim zestawem znaków może być np. (na początku jest spacja):

 .:-;!/>)|&IH%*#

Innym podejściem jest rozpoznawanie układu jaśniejszych i ciemniejszych podobszarów takiego prostokąta (po podzieleniu go np. na 4, 6, 8 czy 9 części) i znajdowanie znaku o podobnym kształcie. To pozwoliłoby lepiej oddawać cienkie linie oraz krawędzie obiektów.

Mimo wszystko nie wygląda to najlepiej. Jednak na szczęście zamiana grupy pikseli na pewien znak to nie jedyne, co można osiągnąć za pomocą tekstu. Do niektórych rzeczy potrzebna jest ludzka wyobraźnia, nie wystarczy dobry algorytm. Efekty są wtedy dużo lepsze.

Grafika

Wykorzystując kształt różnych znaków można w małym obszarze zamknąć skomplikowany, ale całkiem czytelny obrazek:

                   _.-+.
              _.-""     '.
          +:""            '.
          J \               '.
           L \             _.-+
           |  '.       _.-"   |
           J    \  _.-"       L
            L    +"          J
            +    |           |   -Henry Segerman-
             \   |          .+
              \  |       .-'
               \ |    .-'
                \| .-'
                 +

(Prezentowane grafiki nie są mojego autorstwa, pochodzą serwisów na temat ASCII Art) Oprócz linii, można też znaleźć sposób na rysowanie wypełnień:

           ..ed$$$$$be..
         .d$$$$$$$$$$$$$$c
       .$$$$P"" $$$ ""*$$$$c
      z$$$*"    $$$     *$$$e
     z$$$"      $$$      ^$$$L
     $$$F       $$$       '$$$
    4$$$       d$$$$.      $$$F
    4$$$     .$$$$$$$e     $$$F
     $$$r   d$$P$$$$$$$.  .$$$"
     *$$$..$$$" $$$ "$$$e $$$P
      *$$$$$P"  $$$  ^*$$$$$$
       *$$$$c.  $$$  .z$$$$P
        ^*$$$$$$$$$$$$$$$P"
           "*$$$$$$$$$*""
               """""     Gilo94'

Tekst

Dzięki ASCII Art wszędzie tam, gdzie dostępny jest tylko jeden krój i wielkość czcionki, można tworzyć duże, ozdobne napisy. Oto przykłady napisów utworzonych w programie Email Effects:

 _____    _        _                _
|_   _|__| | _____| |_   _ __ _   _| | ___ ____
  | |/ _ \ |/ / __| __| | '__| | | | |/ _ \_  /
  | |  __/   <\__ \ |_  | |  | |_| | |  __// /
  |_|\___|_|\_\___/\__| |_|   \__,_|_|\___/___|

  ______     __        __                __
 /_  __/__  / /_______/ /_   _______  __/ /__  ____
  / / / _ \/ //_/ ___/ __/  / ___/ / / / / _ \/_  /
 / / /  __/ ,< (__  ) /_   / /  / /_/ / /  __/ / /_
/_/  \___/_/|_/____/\__/  /_/   \__,_/_/\___/ /___/

Linki

Email Effects
Bardzo dobry i wygodny program do tworzenia ASCII Art, w tym ładnych napisów (niestety shareware)
ASCII Generator
Bardzo rozbudowany program do zamiany grafiki na ASCII Art (Uwaga! Na tej stronie jest dużo wyskakujących reklam oraz próbujący się zainstalować szpieg Gator firmy GAIN!)
chris.com - ASCII Art Collection
Obszerna kolekcja obrazków ASCII oraz artykuły na temat ASCII Art (szczególnie polecam artykuł Rowana Crawforda)
Ascii Art Dictionary
Duża kolekcja ASCII Art

Gry tekstowe

Istnieją gry o charakterze całkowicie tekstowym. Choć z oczywistych powodów nie są zbyt popularne, warto zainteresować się nimi. Brak multimediów rekompensuje skomplikowany świat gry i ogromna grywalność. Dlatego są to zazwyczaj gry typu RPG (choć ich gatunek określają specjalne nazwy). Te gry naprawdę wciągają, wręcz uzależniają.

Inną ich cechą jest niekomercyjny charakter. Dostępne są zazwyczaj za darmo, nierzadko razem z kodem źródłowym. Stanowią jakby alternatywny nurt wobec komercyjnych superprodukcji.

Rougelike

Oto oryginalna definicja gatunku Rougelike pochodząca z gry ADOM:

A rogue-like game is a turnbased single-user game featuring the exploration of a dungeon complex. You control a fictional character, often described by race, class, attributes, skills, and equipment. This fictional character is trying to achieve a specific goal and succeed in a difficult quest. To fulfill the quest, you have to explore previously undiscovered tunnels and dungeons, fight hideous monsters, uncover long forgotten secrets, and find treasures of all kind. During the game, you explore dungeon levels which are randomly generated each game. You might also encounter certain special levels, which present a particular challenge or are built around a certain theme.

The term rogue-like comes from the game Rogue. Rogue was developed in the late 70's and released in the early 80's and is considered the mother of all rogue-like games. Since rogue-like games have their roots in the 80's many of the current rogue-likes still use very simple graphics or plain ASCII characters to represent the dungeons and other locations in the game.

Rougelike to gry RPG. Gracz wciela się w postać posiadającą swoje imię, rasę, różne współczynniki i umiejętności oraz ekwipunek (posiadane przedmioty) i porusza się po mapie odwiedzając różne miejsca, walcząc z potworami itp. Mapa jest złożona z kolorowych znaczków, dwuwymiarowa, pokazana w widoku od góry. Gra jest przeznaczona dla jednego gracza i turowa w taki sposób, że wszystkie postacie poruszają się w momencie, kiedy gracz wykonuje ruch.

Zagadnienia istotne podczas pisania gry tego rodzaju to m.in.:

  1. Mapa (środowisko - przyroda, a także wnętrza budynków, podziemia itp.)
  2. Bohater - jego cechy, współczynniki
  3. Inwentarz (ekwipunek) i przedmioty
  4. Umiejętności i magia (czary)
  5. NPC (Non Player Character) - potwory i inne postacie, ich sztuczna inteligencja
  6. Walka
  7. Interfejs użytkownika, obsługa, sterowanie i inne zagadnienia tzw. gameplay
  8. Wyświetlanie (grafika)
  9. LOS (Line Of Sight) - pokazywanie tylko tych potworów, które są w zasięgu widzenia bohatera z uwzględnieniem zasłaniania przez różne obiekty

Ponadto w grach Rougelike wiele (o ile nie wszystko) generowane jest przez algorytmy, a nie zapisane w utworzonych wcześniej plikach. Generowany może być teren, budynki, rozmieszczenie potworów, a nawet imiona postaci. Algorytmy generowania powinny posiadać element losowy i dawać dobre efekty.

Oto zrzut ekranu z gry ADOM uruchomionej na konsoli:

Zobacz obrazek

Linki

Dungeondweller (roguelikedevelopment.org)
Serwis w całości poświęcony tworzeniu gier Rougelike, zawiera obszerny zbiór artykułów na ten temat
ADOM - Ancient Domains Of Mystery
Jedna z najbardziej znanych gier Rougelike
Serwis ADOM
Polska strona poświęcona grze ADOM
The Official Angband Home Page
Angband - inna gra Rougelike

MUD

MUD (Multi User Dungeon) to sieciowa gra tekstowa całkowicie pozbawiona widoku mapy. Gracz czyta jedynie opisy miejsc, między którymi przechodzi, obiektów, którym chce się przyjrzeć oraz postaci, które spotyka. Porusza się i wszelkie inne czynności wykonuje za pomocą specjalnych poleceń.

Charakterystyczne dla gier MUD jest to, że działają one wyłącznie przez sieć. Nie są to jednak gry typu host-join, gdzie jeden gracz zakłada grę, kilku innych dołącza się i odbywa się rozgrywka. Każdy MUD ma jeden wielki świat gry, całkowicie liczony na serwerze i gracze przyłączając się do niego grają w nim wspólnie.

Podobnie jak Rougelike, są to gry RPG osadzone zazwyczaj w świecie fantasy. Gracze sterują swoimi postaciami, zdobywają coraz wyższe współczynniki i coraz lepsze przedmioty, a także walczą z potworami i ze sobą nawzajem. Mogą też oczywiście rozmawiać i robić wiele różnych innych rzeczy.

Wygląda na to, ze dopiero obecnie komercyjni producenci "multimedialnych" gier zaczynają dostrzegać zalety płynące z takiego podejścia do gier sieciowych. Owocuje to powstawaniem wielu gier określanych jako MMORPG (Massive Multiplayer Online Role Playing Game). Dla nich jednak to okazja, by wprowadzić opłaty abonamentowe za korzystanie z serwera. Tymczasem w MUDy można grać za darmo i najczęściej wystarczy do tego dowolny klient Telnet - nie trzeba nawet uruchamiać żadnego osobnego programu!

Linki

Argadia
Cygnus Division
LAC MUD
Kuźnia Dusz
Polskie gry MUD
Smaug - Mud Design
Silnik do tworzenia własnych gier MUD

Otchłań

Otchłań to gra tekstowa bez widoku mapy podobna do gier MUD. W odróżnieniu od nich jednak ta w pełni polska produkcja działa jako osobna aplikacja i przeznaczona jest do gry lokalnej - dla jednego gracza. Oto zrzut ekranu z tej gry:

Zobacz obrazek

Linki

Strona domowa Otchłani
Oficjalna strona gry
Świat Otchłani
Fajna strona o Otchłani (aktualnie nie działa)
Imperium Otchłani
Otchłań PageZ by Cet
Inne strony poświęcone tej grze

Język naturalny

Oprócz poleceń formułowanych dla komputera i komunikatów otrzymywanych od niego, komputer może też w pewien sposób obsługiwać tekst pisany w języku naturalnym (np. polskim czy angielskim) - choćby tylko go przesyłać.

Komunikacja

Komunikacja międzyludzka jest jednym z zastosowań tekstu. Taki sposób wykorzystania Internetu był jednym z pierwszych i nadal pozostaje jednym z podstawowych - mimo wielu pojawiających się niedogodności, jak coraz większa plaga spamu.

Jest to o tyle ważne, że cyfrowemu medium przesyłania informacji daleko jeszcze do wygody, z jaką używa się tradycyjnego telefonu. Podejmowane są jednak próby zapewnienia wygodnej komunikacji głosowej, jak darmowy, głosowy komunikator P2P - Skype.

Komunikację za pomocą tekstu można podzielić na:

Synchroniczną
Rozmowa odbywa się w czasie rzeczywistym poprzez wymianę krótkich wypowiedzi, np.: IRC, Instant Messaging (komunikatory, jak Gadu-Gadu), chaty przez WWW
Asynchroniczną
Na serwerze zapisywane są dłuższe wiadomości tekstowe i inne osoby mogą je później czytać, np.: e-mail, grupy dyskusyjne, fora przez WWW

Komunikacja tekstowa przez Internet spowodowała wykształcenie charakterystycznego języka, zestawu skrótów, symboli i innych zasad. Np. często nie używa się polskich liter, a czasami nawet dużych liter i interpunkcji. W powszechnym użyciu są za to uśmieszki (emotikonki).

Ta sieciowa "nowomowa" bywa przez niektórych potępiana. Nie rozumieją oni, że tekst tego rodzaju ma pełnić taką rolę, jak głos w czasie zwykłych rozmów, a nie jak słowo pisane w książkach czy artykułach.

Warto przy okazji wspomnieć o botach. Boty, szczególnie powszechne na IRCu, to programy działające na takich samych zasadach jak zwykli użytkownicy - siedzą na danych kanałach, mają swoją ksywę itd. Spełniają zawsze określone funkcje, np. pilnują kanału i porządku na nim albo świadczą użytkownikom pewne usługi.

Linki

Eggheads.org
Eggdrop - najpopularniejszy, uniwersalny bot IRC
Skype
Darmowy, głosowy komunikator P2P, który "po prostu działa"

Chatterboty

<AnA> CZESC
<Regedit> JAK SIĘ NAZYWASZ?
<AnA> JA NAZYWAM SIE ANA
<Regedit> A ILE MASZ LAT?
<AnA> O JESTEM MLODA MAM 22 LATA

To nie początek podrywu na IRC, ale krótki fragment rozmowy z programem. Chatterboty to aplikacje sztucznej inteligencji, których zadaniem jest rozmowa z człowiekiem za pomocą tekstu. AnA jest jedną z nich.

Zadanie napisania programu, który potrafiłby prowadzić inteligentną konwersację wydaje się niemożliwe i póki co, faktycznie takie jest. Już niejeden program próbował zaliczyć tzw. test Turinga i jak na razie żadnemu się nie udało. Zadanie polega na tym, aby rozmówca nie był w stanie stwierdzić, że rozmawia z komputerem a nie z człowiekiem. Nie chodzi jednak o to, by stworzyć sztucznego człowieka. Chatterbot to pewna forma komunikacji człowieka z maszyną. W obecnym stanie pełni raczej rolę ciekawostki. Boty tworzone są zwykle przez pasjonatów i służą jedynie rozrywce.

Jednak przewiduje się ich zastosowania np. w roli cyfrowego przedstawiciela firmy. Łatwiej będzie klientowi odwiedzającemu stronę WWW po prostu spytać o coś bota, niż szukać informacji po rozbudowanym serwisie. Przykładem takiego bota jest Fido. Pod względem technicznym wydaje się dość prymitywny. Oprócz opowiadania o sobie i swojej firmie potrafi pokazywać dowcipy ze swojej bazy, a podczas pożegnania prosi o podanie adresu e-mail (ciekawe po co? :P).

Jak działa chatterbot? Możliwości jego budowy jest bardzo wiele - od najprostszych po bardzo skomplikowane. Różnią się one uzyskiwanymi efektami, jednak żadne nie pozwalają choćby zbliżyć się do poziomu reprezentowanego przez człowieka. Oto garść możliwych rozwiązań:

  • Rozpoznawanie słów kluczowych na wejściu i w reakcji na nie wypisywanie na wyjście jednej z predefiniowanych odpowiedzi.
  • Rozpoznawanie rodzaju wypowiedzi (czy to jest pytanie) poprzez znajdowanie znaku "?" oraz słów takich jak "czy", "kto", "ile" itp.
  • Rozpoznawanie emocji wyrażonych w wypowiedzi, np. poprzez znajdowanie wykrzykników i uśmieszków.
  • Próba rozpoznania, co jest aktualnym tematem dyskusji (podmiotem).
  • Uwzględnianie także poprzednich wypowiedzi, a nie tylko ostatniego pytania (kontekst).
  • Zadawanie pytań, a nie także udzielanie odpowiedzi. Podtrzymywanie rozmowy.
  • Stosowanie różnych trików psychologicznych i odpowiedzi wymijających, ogólnych.

Wiara w to, że zastosowanie modnych technik, jak sieci neuronowe czy algorytmy genetyczne spowoduje niespodziewanie, że komputer zacznie myśleć, jest naiwna. W praktyce lepiej sprawdzają się chyba rozwiązania tradycyjne oraz solidne, skomplikowane algorytmy, ewentualnie systemy ekspertowe.

Ciekawym pomysłem byłoby uznanie, że bot nigdy nie dorówna człowiekowi i napisanie go w taki sposób, aby mógł być traktowany jako "niższa forma życia". Użytkownikom mógłby się taki program spodobać, ponieważ dostarczałby rozrywki i sympatia do niego oparta byłaby na tych samych ludzkich instynktach, dzięki którym lubimy hodować psy, chomiki i inne zwierzątka.

Jeśli chodzi o dane przechowywane przez bota, może to być np. baza słów kluczowych i odpowiedzi lub ich fragmentów. Ogólna zasada jest taka, że im bardziej próbuje się stworzyć bota inteligentnego (samodzielnie generującego odpowiedzi z małych fragmentów, uczącego się), tym mniej sensowne i składne są jego wypowiedzi.

Istota problemu jest oczywista. Chatterbot działa w taki sposób, w jaki działałby np. człowiek nauczony reagować wg pewnego algorytmu na pewne wyrazy jakiegoś języka nie znając go i nie rozumiejąc, o czym mówi. Aby napisać chatterbota potrafiącego zdać test Turinga, trzebaby chyba zaimplementować takie składniki:

  1. Analizę otrzymywanych wypowiedzi i odwrotnie - składanie swoich odpowiedzi z poszczególnych wyrazów. Takie teksty reprezentowane byłyby w pamięci w postaci drzewa, tak jak wygląda rozkład gramatyczny zdania, którego uczą w szkole podstawowej. Bot musiałby znać wszystkie wyrazy i ich formy gramatyczne, co jest szczególnie trudne w przypadku języka polskiego.
  2. Rozumienie wypowiedzi i wnioskowanie - bot musiałby oprócz umiejętności posługiwania się językiem także posiadać informacje o świecie lub pewnej dziedzinie wiedzy i prawidłowo z nich korzystać - czyli po prostu myśleć!

Jednak nawet implementując te możliwości pozostałby szereg problemów:

  • Nierozumienie wypowiedzi niekompletnych czy zawierających błędy (chociażby literówki)
  • Powinien posiadać własne opinie i poglądy
  • Powinien posiadać emocje
  • Powinien posiadać poczucie humoru
  • Powinien posiadać poczucie czasu i miejsca oraz zbierać informacje na temat różnych obiektów i osób

Mimo tego chatterboty pozostają przedmiotem zainteresowania wszystkich - od programistów-pasjonatów, poprzez firmy, aż po ośrodki naukowe.

AnA wirtualna kobieta
Polski chatterbot
The A.L.I.C.E. Artifical Intelligence Foundation
Chatterbot rozmawiający po angielsku - bardzo poważny projekt
fido interactive
Firma zajmująca się komercyjnym zastosowaniem chatterbotów i jej bot - Fido

Tłumaczenie tekstu

Innym zagadnieniem związanym z cyfrowym przetwarzaniem języka naturalnego jest tworzenie programów tłumaczących całe teksty pomiędzy różnymi językami. Programów takich powstaje wiele i są one coraz lepsze. Jednak nadal jakość uzyskiwanych tekstów pozostawia wiele do życzenia, a programy tego typu nie mogą zastąpić żywych tłumaczy.

Oto przykład tłumaczenia pierwszych zdań polskiej wersji licencji GNU GPL za pomocą programu Komputerowy Tłumacz Języka Angielskiego firmy Kompas:

Tekst po polsku:
Większość licencji na oprogramowanie pomyślana jest po to, aby odebrać użytkownikowi możliwość swobodnego udostępniania innym i zmieniania danego software'u. Natomiast w wypadku Powszechnej Licencji Publicznej GNU (GNU General Public License, GPL) celem jest zagwarantowanie użytkownikowi swobody udostępniania i zmieniania tego wolnego oprogramowania, a więc danie pewności, iż oprogramowanie jest wolno dostępne dla wszystkich użytkowników.

Przekład na angielski: The most of the licence on the software conceived is after this , to take the user the possibility of free throwing open with other and changings of the given software'at. Instead in the event of of the General Public Licence THE GNOO (THE GNOO General Public License , GPL) an aim {target} is the guarantee to the user of the freedom of throwing open and changings of this free software , and so the giving {course} of the certainty , that the software is slowly accessible for all users.

Przekład z powrotem na polski: Najbardziej z pozwolenia {licencji} na oprogramowaniu poczęty jest w pogoni za {jest po} tym , by brać użytkownikowi możliwość wolny rzucającego otwartego z innym i changings danego oprogramowania'przy. Zamiast tego w wypadku Ogólnego Publicznego Pozwolenia {Licencji} GNU (GNU Ogólne Publiczne Pozwolenie , GPL) cel {celu} jest gwarancja {gwarant} do użytkownika wolności rzucania otwartego i changings tego oprogramowania darmowego , i tak dający {bieg {kurs}} pewności , że oprogramowanie jest powoli dostępne dla wszystkich użytkowników.

Najprostszym podejściem do zadania jest bezpośrednie tłumaczenie wyrazów za pomocą słownika. Pojawia się jednak szereg problemów:

  • Jeden wyraz ma kilka znaczeń - należałoby wybrać odpowiednie na podstawie kontekstu
  • Należałoby uwzględnić terminologię specjalistyczną, np. w informatyce "driver" znaczy "sterownik", a nie "kierowca"
  • W różnych językach (szczególnie tak różnych, jak polski i angielski) obowiązuje różny szyk zdania
  • Nie wiadomo, czy dany zwrot nie jest nazwą własną - żeby np. Red Hat nie został "czerwonym kapeluszem" :)
  • W języku polskim wyrazy posiadają różne formy i odmiany - bez uch uwzględnienia wychodzi "Kali zjeść słonia" :)

Moim zdaniem, rozwiązaniem idealnym byłoby utworzenie uniwersalnego formatu reprezentującego pojęcia i ich powiązania, niezależnego od żadnego języka. Gdyby udało się przetwarzać różne języki na ten format w obydwie strony, raz na zawsze zniknęłaby bariera językowa, a stałoby się to bez uszczerbku na bogactwie różnych języków świata. Pomogłoby też znacznie w tworzeniu chatterbotów.

Rozwój informatyki, a także wzrost szybkości komputerów i rozmiaru pamięci już nie raz pozwolił na pokonanie różnych ograniczeń i tym samym napisanie rzeczy wcześniej niemożliwych. Dlatego wierzę, że kiedyś także w sprawie języka naturalnego komputery dużo nam pomogą...

Ciekawostka

Postaraj się przeczytać poniższy tekst z normalną szybkością i zrozumieć go - tak jak się normalnie czyta - nie skupiając się na poszczególnych literach:

Zdognie z nanjwoymszi baniadmai perzporawdzomyni na bytyrijskch uweniretasytch nie ma zenacznia kojnoleść ltier przy zpiasie dengao sołwa. Nwajżanszjie jset, aby prieszwa i otatsnia ltiera błya na siwom mijsecu, ptzosałoe mgoą być w nieaedziłe i w dszalym cąigu nie pwinono to sawrztać polbemórw ze zozumierniem tksetu. Dzijee się tak datgelo, że nie czamyty wyszistkch lteir w wazyrie, ale cłae sołwa od razu. Młeigo dina.

Czyżby pojawiła się nowa koncepcja stratnej kompresji tekstu? :)))

Wyszukiwanie

W wielu zastosowaniach zachodzi potrzeba przeszukiwania tekstu. W najprostszym przypadku polega ono na stwierdzeniu obecności bądź nieobecności danego wyrazu w danym tekście. Jednak temat jest dużo bardziej obszerny i złożony. Wyszukiwać można np. pliki na dysku. Każdy edytor tekstu ma funkcję "Znajdź i zamień...". Wyszukiwarki internetowe, z których korzystamy na co dzień, także dokonują przeszukiwania tekstu.

Wildcards

Wildcards (symbole wieloznaczne) to wyszukiwanie tylko o krok bardziej złożone, niż zwykłe poszukiwanie podanego tekstu. Znamy je wszyscy doskonale - służy nam bowiem do wyszukiwania plików już od czasów systemu DOS. *.exe to właśnie wildcards.

Zasada działania polega na sprawdzaniu, czy podany tekst (lub jego fragment) pasuje do podanej maski. Maską jest łańcuch, którego znaki interpretowane są dosłownie i porównywane ze znakami przeszukiwanego tekstu za wyjątkiem znaków specjalnych zwanych symbolami wieloznacznymi:

  1. ? - oznacza jeden, dowolny znak
  2. * - oznacza dowolną ilość (także 0) dowolnych znaków

Oto przykłady masek i pasujących do nich łańcuchów:

?      a
       X

??a*   XXa
       12aBLEBLEBLE

*.*    .
       coś1.
       coś1.coś2

p*e    pe
       programowanie
       pie______e

Zaprezentuję teraz przykładową implementację funkcji w C++, która sprawdza, czy podany łańcuch jako całość pasuje do podanej maski:

bool ValidateWildcard(const std::string &mask, const std::string &s,
  bool CaseSensitivity)
{
  size_t j;
  size_t maskl = mask.size();
  size_t sl = s.size();

  // dla każdego znaku maski...
  for (size_t i = 0; i < maskl; ++i)
  {
    // rekurencja
    if (mask[i] == '*')
    {
      for (j = i; j < sl+1; ++j)
        if (ValidateWildcard(mask.substr(i+1, maskl-1), s.substr(j,
 sl-j+1),
          CaseSensitivity))
          return true;
      return false;
    }
    // jeśli znak maski wykracza poza znak łańcucha - false
    else if (i >= sl)
      return false;
    // jeśli znaki łańcucha wykraczają poza maskę - false
    else if ( (i == maskl-1) && (sl > maskl) )
      return false;
    // porównanie znaków
    // - '?' - znak w stringu może być dowolny - można olać sprawę :)
    else if (mask[i] == '?')
    {
      // Nic
    }
    // - inny znak - musi się zgadzać ze znakiem łańcucha
    else if (
      ( (mask[i] != s[i]) && CaseSensitivity ) ||
      ( CharUpper((char*)mask[i]) != (CharUpper((char*)s[i])) &&
        !CaseSensitivity)
    )
    return false;
  }
  return true;
}

Jako symbole wieloznaczne mogą być używane inne znaki, np. w SQL pojedynczy znak zastępuje podkreślenie "_", a dowolny ciąg znaków procent "%".

Wyrażenia regularne

Wyrażenie regularne (ang. "regular expression" - w skrócie "regexp") to sposób przeszukiwania tekstu bardziej zaawansowany, niż wildcards. Działa na podobnej zasadzie, ale pozwala używać bardzo wielu różnych znaków o specjalnym znaczeniu. Przeszukiwany łańcuch jest lub jego fragment jest dopasowywany do podanego wyrażenia zwanego też wzorem (ang. "pattern").

Oprócz przeszukiwania można dzięki nim wybierać pewne elementy z wnętrza łańcucha (parsować go), dokonywać jego dzielenia na części, zastępować pewne fragmenty innymi itp. Wyrażenia regularne są bardzo potężne. Osobie nieznającej ich składni się jednak wydawać przerażające, np.:

/(<([\w]+)[^>]*>)(.*)(<\/\\2>)/

Jak się jednak już zaraz okaże, ich składni można się bardzo szybko nauczyć.

Istnieją implementacje kilku standardów wyrażeń regularnych. Jedną z nich jest biblioteka PCRE rozprowadzana na licencji Open Source, której autorem jest Philip Hazel. Implementuje ona standard używany w języku Perl, a także w PHP. Inną bibliotekę napisał Henry Spencer. Jest ona nastawiona na zgodność ze standardem POSIX 1003.2 i używana m.in. w MySQL, TCL, a także w PHP.

Podstawowe zasady i najważniejsze używane znaki specjalne są w każdym z tych standardów podobne. W Internecie znajdziesz wiele opisów składni wyrażeń regularnych. Są dołączane praktycznie do każdego oprogramowania, które ich używa. Dlatego opiszę tylko niektóre jej elementy.

Wyrażenia regularne obejmuje się w ukośniki "/ ... /". To dość egzotyczna odmiana nawiasu :) Istnieje szereg modyfikatorów stosowanych w celu zmiany standardowego zachowania parsera wyrażeń regularnych, np. nakazanie mu, by nie rozróżniał wielkości liter.

Najprostsze wyrażenie nie zawiera żadnych znaków specjalnych. Np. wyrażenie "hello" pasuje tylko do łańcucha "hello" i do żadnego innego. Każda litera takiego wyrażenia jest tzw. atomem. Po każdym atomie może wystąpić specjalny znak określający dopuszczalną ilość jego powtórzeń:

brak
dokładnie jedno
?
zero lub jedno
+
jedno lub więcej
*
zero lub więcej
{m}
dokładnie m
{m,}
m lub więcej
{m,n}
co najmniej m i co najwyżej n

Oto przykład wyrażeń i pasujących do nich łańcuchów:

        xAy  xA?y  xA+y  xA*y  xA{3}y
------------------------------------------
xy            X           X
xAy      X    X     X     X
xAAAy               X     X     X

Odwrócony ukośnik służy do poprzedzania innych znaków i nadaje im specjalne znaczenie:

\X
dosłowne potraktowanie znaku X (może to być dowolny znak) - nie jako znak specjalny
\r, \n, \t itp.
tak jak w C++ i wielu innych językach; tu odpowiednio: znak powrotu karetki, końca wiersza i poziomej tabulacji
\DDD, \xHH
znak o kodzie DDD zapisanym dziesiętnie lub HH zapisanym szesnastkowo

Atomem może być też taki element, który pasuje do kilku różnych znaków:

[az]
Pasuje do niego litera "a" lub "z"
[a-z]
Pasuje do niego każda litera pomiędzy "a" i "z" włącznie
[^az]
Pasuje do niego każdy znak z wyjątkiem litery "a" i "z"

Istnieją też predefiniowane znaki specjalne określające pewne grupy znaków, np.:

\d
Każda cyfra dziesiętna
\D
Każdy znak, który nie jest cyfrą dziesiętną
\s
Każdy biały znak
\S
Każdy znak, który nie jest biały

Osobno wymienić trzeba dwa specjalne znaki:

^
Oznacza, że w tym miejscu musi być początek łańcucha
$
Oznacza, że w tym miejscu musi być koniec łańcucha

Oczywiście różne elementy składni wyrażeń regularnych można ze sobą łączyć. Na przykład wzór:

^p[^a-dX]+e$

Pasuje do każdego łańcucha, który rozpoczyna się od litery "p", kończy się literą "e", a pomiędzy nimi ma co najmniej jeden znak. Przy tym wszystkie znaki pomiędzy nimi muszą być różne od "a", "b", "c", "d" i "X".

Kropka zastępuje dowolny znak. Pionowa kreska "|" służy do oddzielania kilku alternatywnych łańcuchów, np. do wzoru:

pl|en

pasuje łańcuch "pl", jak również łańcuch "en".

Asercja to specjalny element, który nie powoduje odczytania nowego znaku, a jedynie warunkuje wystąpienie w danym miejscu określonej sytuacji, np.:

\b
W tym miejscu musi być granica słowa
\B
W tym miejscu nie może być granicy słowa

Na koniec trzeba wspomnieć o zwykłych nawiasach, które służą do tworzenia podwyrażeń. Łańcuch dopasowany do takiego podwyrażenia zostanie zwrócony jako jeden z wyników przetwarzania wyrażenia regularnego (może być więcej takich podwyrażeń). Aby utworzyć podwyrażenie (np. w celu użycia kilku alternatywnych łańcuchów oddzielonych pionową kreską "|"), a nie spowodować przy tym zwrócenia jego treści, użyj takiego nawiasu:

(?: ... )

Na zakończenie pozwolę sobie przytoczyć przykład zaczerpnięty z podręcznika PHP pokazujący zastosowanie pokazanego na początku tego podrozdziału wyrażenia regularnego. Poniższy kod parsuje przekazany łańcuch w postaci kodu HTML na zbiór znaczników. Każdy z rezultatów składa się z rozpoczęcia, treści i zakończenia.

$html = "<b>bold text</b><a href=howdy.html>click me</a>";

preg_match_all("/(<([\w]+)[^>]*>)(.*)(<\/\\2>)/", $html, $matches);

for ($i=0; $i < count($matches[0]); $i++) {
  echo "matched: " . $matches[0][$i] . "\n";
  echo "part 1: " . $matches[1][$i] . "\n";
  echo "part 2: " . $matches[3][$i] . "\n";
  echo "part 3: " . $matches[4][$i] . "\n\n";
}

A oto wynik jego działania:

matched: <b>bold text</b>
part 1: <b>
part 2: bold text
part 3: </b>

matched: <a href=howdy.html>click me</a>
part 1: <a href=howdy.html>
part 2: click me
part 3: </a>

Linki

PCRE
Biblioteka implementująca standard Perla

Przeszukiwanie tekstu

Temat przeszukiwania tekstu to nie tylko sprawdzanie zgodności łańcucha z podanym wzorcem. Spróbujmy sobie wyobrazić, jak działa wyszukiwarka internetowa. Musi posiadać bazę zapisanych w jakiejś postaci informacji o znanych stronach WWW, wydajnie ją przeszukiwać, a użytkownikowi udostępniać możliwość wygodnego zadawania zapytań i czytelnie prezentować wyniki.

Słowa kluczowe

Najprostszym rozwiązaniem katalogowania dużej ilości zasobów jest przypisywanie każdemu z nich zbioru słów kluczowych. Np. pewna książka w bibliotece może mieć słowa kluczowe:

C++,programowanie,język

Użytkownik miałby możliwość wyszukiwania książek, w których występują podane przez niego słowa kluczowe.

Wyrażenia logiczne

Ważny jest sposób zadawania zapytań. Dawniej bardzo powszechne były wyrażenia używające spójników logicznych, np. wyrażenie:

programowanie AND ("C++" OR Pascal) AND NOT Basic

spowodowałoby wyszukiwanie wszystkich tych pozycji na temat programowania, które traktują o języku C++ lub Pascal, ale w których nie ma ani słowa o języku Basic.

Częściej spotykane spójniki to:

x AND y
i
x OR y
lub
NOT x
nie
x NEAR y
obok, w pobliżu
+x
podany wyraz musi wystąpić
-x
podany wyraz nie może wystąpić

Ponadto można używać cudzysłowów do obejmowania łańcuchów zawierających dowolne znaki i odstępy, żeby zostały potraktowane dosłownie. Można też ustalać kolejność wykonywania działań za pomocą nawiasów.

Nowoczesne wyszukiwanie

Wydaje się, że obecnie panuje tendencja, by odchodzić od tych niezbyt przystępnych wyrażeń logicznych. Zamiast nich, daje się użytkownikowi możliwość wpisania listy wyrazów do wyszukania - jeden za drugim. Ewentualnie można je obejmować w cudzysłowy.

Cały problem polega wtedy na tym, by odpowiednio skonstruować listę wyników wyszukiwania - zwłaszcza właściwie posortować jej pozycje względem pewnego współczynnika - trafności. Użytkownicy nie mają zwyczaju zaglądania na dalsze strony listy z wynikami i sedno sprawy powinni mieć od razu na pierwszym miejscu.

Chyba właśnie to zdecydowało o sukcesie wyszukiwarki Google. Po wpisaniu nazwy jakiegoś programu na pierwszym miejscu dostajemy link do jego strony głównej, a po wpisaniu jakiegoś ogólnego tematu - linki do największych poświęconych mu serwisów. Inne wyszukiwarki mają zwyczaj prezentować w tym miejscu szereg bezużytecznych stron, w których przypadkiem wystąpiło szukane słowo.

Powstaje pytanie, w jaki sposób szacować trafność znalezionych stron? Poniżej prezentuję przykładową funkcję dokonującą wyszukiwania łańcucha podanego jako pierwszy parametr w łańcuchu podanym jako drugi parametr. Funkcja zwraca liczbę zmiennoprzecinkową oznaczającą "trafność". Będzie to 0, jeśli szukane wyrażenie nie zostało znalezione lub liczba dodatnia, jeśli zostało znalezione. Jak duża będzie to liczba? To zależy... Zwykle ok. 1-10.

Mówiąc ogólnie, zwrócona trafność jest tym większa, im:

  • Poszukiwane wyrażenie jest dłuższe
  • Przeszukiwany tekst jest krótszy
  • Poszukiwane wyrażenie zostanie znalezione więcej razy
  • Znalezione wyrażenie ma taką samą wielość liter, jak poszukiwane
  • Znalezione wyrażenie jest całym ciągiem tzn. np. "Fajna GRA strategiczna", a nie "proGRAmowanie"
  • Wyrażenie znalezione zostanie na początku przeszukiwanego tekstu
  • Wyszukiwane wyrażenie stanowi dokładnie treść przeszukiwanego tekstu
// Funkcja do użytku wewnętrznego dla FineSearch
// Zwraca liczbę zależną od okoliczności, w jakich występuje
// znaleziony string.
// Mnożona jest przez nią obliczana trafność.
float _MnoznikOkolicznosci(const std::string &str, size_t start,
 size_t length)
{
  bool l = (start == 0);
  bool r = (start+length == str.size());
  // Całe pole
  if (l && r)
    return 4.0f;
  // Początek pola
  else if (l)
    return 3.0f;
  else
  {
//    if (!l)
    if (IsCharAlphaNumeric(str[start-1]))
      l = true;
    if (!r)
      if (IsCharAlphaNumeric(str[start+length]))
        r = true;
    // Cały ciąg
    if (l && r)
      return 2.0f;
    // Nic ciekawego
    else
      return 1.0f;
  }
}

float FineSearch(const std::string &asubstr, const std::string &astr)
{
  std::string substr = asubstr;
  std::string str = astr;
  std::string usubstr = LowerCase(substr);
  std::string ustr = LowerCase(str);
  float result = 0.0f;
  size_t p;
  float x;
  while (true)
  {
    p = ustr.find(usubstr);
    // Koniec szukania
    if (p == std::string::npos)
      break;
    x = max( usubstr.size()/5.0f - astr.size()/100.0f + 1.0f, 1.0f);
    // Jeśli zgadza się wielkość liter
    if (str.substr(p, substr.size()) == substr)
      x *= 1.5;
    // Mnożnik okoliczności - jeśli to całe pole,
    // początek pola lub cały ciąg
    x *= _MnoznikOkolicznosci(astr, p+astr.size()-str.size(),
 substr.size());
    // Usuwamy co przerobione i od nowa :)
    str = str.substr(p+substr.size());
    ustr = ustr.substr(p+substr.size());
    // No i oczywiście dodajemy wyliczoną trafność
    result += x;
  }
  return result;
}

Funkcję wyszukującą trzeba wywołać wiele razy, by każde pole każdego rekordu przeszukać w poszukiwaniu każdego z wyrażeń wpisanych w zapytaniu przez użytkownika. Otrzymaną trafność należy dla każdego rekordu sumować.

Oto przykład użycia. Zakładamy, że funkcja ma za zadanie przeszukać pojedynczy rekord danych. Składa się on z dwóch pól nazwanych Pole1 i Pole2. Query to jakiś obiekt przechowujący listę wyrazów do wyszukania.

float trafnosc = 0;
for (int i = 0; i < Query.Count-1; ++i)
{
  trafnosc = trafnosc + 2.0f * FineSearch(Query[i], Pole1);
  trafnosc = trafnosc + 1.0f * FineSearch(Query[i], Pole2);
}

Jak widać, obydwa pola rekordu przeszukiwane są w poszukiwaniu każdego z wyrażeń zapytania (Query). Trafność jest sumowana.

Poszczególne pola mogą mieć różne znaczenie, dlatego warto nadawać im współczynniki wagowe. Zakładamy, że Pole1 to nazwa (tytuł - a więc ważny) rekordu, a Pole2 to jakaś tam jego treść (czyli mniej ważna). Jak widać na powyższym przykładzie, polu pierwszemu nadajemy wagę 2 razy większą, niż drugiemu.

Wyszukane rekordy powinny być sortowane wg trafności. Nie ma jakiegoś maksymalnego limitu trafności. Jeśli chcesz, możesz już po zakończeniu wyszukiwania przeliczyć je na procenty przeskalowując ich wartość tak, by rekord o największej trafności otrzymał 100, a pozostałe proporcjonalnie mniej.

Jeśli przeszukujesz jakąś strukturę hierarchiczną, np. system plików i chcesz, aby wyszukiwanie było jeszcze "fajniejsze", możesz zsumowaną już trafność modyfikować w zależności od poziomu zagłębienia danego rekordu. Przykładowo możesz dzielić trafność przez liczbę poziomów zagłębienia podzieloną przez dwa.

trafnosc = trafnosc / (level/2);

Trafność Rekordu na najwyższym (pierwszym) poziomie podzielona zostanie przez 0.5, czyli pomnożona przez 2. Trafność rekordu na poziomie drugim zostanie podzielona przez 1, czyli pozostanie bez zmian. Na poziomie trzecim podzielona zostanie przez 1.5, na czwartym zmniejszy się dwukrotnie itd.

Jak jeszcze można wzbogacić ten algorytm? Najlepiej byłoby dysponować jakąś liczbą określającą ogólną "wartość" danego rekordu. Można byłoby po prostu pomnożyć przez nią otrzymaną trafność. Może to być np. popularność poszczególnych stron znana choćby z częstości klikania na linki do nich, kiedy pojawiają się w wynikach wyszukiwania twojej wyszukiwarki.

Jest jeszcze jedna kwestia, która pozostaje nierozwiązana. Algorytm mógłby rozpoznawać także wyrazy podobne do szukanych (np. z jedną literą dodatkową, brakującą czy zmienioną), a nie tylko identyczne. Wymagałoby to ułożenia algorytmu oceniającego podobieństwo dwóch wyrazów.

Języki opisu

W formacie tekstowym można przechowywać ustrukturalizowane informacje. Służą do tego specjalnie stworzone tekstowe formaty (lub inaczej języki opisu), jak HTML czy XML. Każdy taki język ma swoje zastosowania. Jedne przeznaczone są do konkretnych celów (jak HTML do tworzenia stron WWW). Inne z kolei są uniwersalne i pozwalają tworzyć własne formaty do przechowywania różnego rodzaju informacji.

Przechowywanie danych w formacie tekstowym ma zarówno wady, jak i zalety. Do wad zaliczyć można na pewno większą objętość danych i większą czasochłonność ich analizy przez komputer, niż miałoby to miejsce w przypadku formatu binarnego. Istnieje jednakże jedna wielka zaleta takich języków - są one tak równie czytelne dla człowieka, co dla maszyny! (czasami równie mało czytelne :)

Jaki musi być tekstowy język opisu? Z pewnością musi posiadać w pełni zdefiniowaną i ściśle określoną składnię. Nie może być żadnych wieloznaczności. Z drugiej strony powinien dopuszczać pewną swobodę w zapisie, aby człowiek piszący taki plik mógł samemu ustalać układ danych.

XML

Istotę i sens tekstowych języków opisu postaram się wyjaśnić na przykładzie języka XML. Znajomość podstaw jego składni z pewnością przyda się niejednokrotnie.

Czym jest XML? Trudno precyzyjnie odpowiedzieć na to pytanie. W zasadzie jest to standard stworzony przez konsorcjum W3C. Znającym język HTML znajome wydadzą się niektóre reguły tego języka. Jednak XML to nie jest z góry określony język. To raczej zbiór reguł, dzięki którym można definiować własne języki.

Wyobraź sobie, że piszesz grę, a do zapisywania rodzajów potworów, jakie w niej wsytępują, używasz XMLa. Przykładowy plik mógłby wyglądać tak:

<?xml version="1.0" encoding="UTF-16" standalone="yes"?>
<potwory>
  <potwór nazwa="Szkielet" obrażenia="10" życie="10"/>
  <potwór nazwa="Zombi" obrażenia="25" życie="16"/>
</potwory>

Dzięki temu nie musiałbyś pisać edytora. Spis potworów mógłbyś stworzyć po prostu w systemowym notatniku lub dowolnym innym edytorze tekstu niesformatowanego. Dopuszczalne nazwy i wzajemny układ elementów (tutaj "potwory" i "potwór"), ich atrybuty (tutaj "nazwa", "obrażenia" i "życie") oraz ogolną strukturę dokumentu definiujesz ty sam, w zależności od potrzeby.

XML to jednak nie tylko nowy format pliku, w pewnym sensie alternatywny względem plików binarnych czy własnych formatów tekstowych. To także pewna idea. W założeniu dokument XML ma przechowywać czyste dane, bez informacji o ich formatowaniu. Inaczej mówiąc - samą treść, a nie formę. Coraz więcej programów wspiera XML. Wydaje się, że właśnie on stanowi przyszłość przechowywania, przesyłania i przetwarzania danych, szczególnie w Internecie.

Przejdźmy teraz do opisu języka XML. Jest to skrót od "Extensible Markup Language", czyli "rozszerzalny język znaczników". Dokument składa się z różnego rodzaju informacji i zbudowany jest w sposób hierarchiczny. Podstawowym rodzajem informacji są elementy, podobnie jak w języku HTML. Mogą być zagnieżdżane jedne w drugich. Jednakże, inaczej niż w HTML, każdy element musi posiadać zamknięcie, a zamknięcia nie mogą się krzyżować. Innymi słowy, element może zawierać w swoim wnętrzu kolejne elementy oraz inne informacje.

Prawidłowy dokument XML rozpoczyna się prologiem. Dalej następuje pojedynczy element główny, który zawiera w swoim wnętrzu wszystkie informacje, jakie dokument ma przechowywać. Element (inaczej znacznik - ang. "tag") może składać się z rozpoczęcia, zawartości i zakończenia lub być od razu domkniętym, jeśli nie posiada zawartości:

<element1>
  tekst jako zawartość elementu
</element1>

<element2/>

Element może też posiadac atrybuty, które są parami łańcuchów oddzielonych znakiem równości. Wartość atrybutu musi być objęta w cydzusłów lub apostrof.

<element nazwa1="wartość1" nazwa2="wartość2">
  <pod-element/>
</element>

Oprócz elementów i ich zawartości dokument XML może zawierać zwyczajny tekst. Jest on, obok wplecionych w niego elementów, częścią elementu nadrzędnego, wewnątrz którego leży. Ponieważ nie wszystkie znaki mogą występować w takim tekście dosłownie, czasami trzeba stosować referencje:

&amp;
Znak amperstand "&"
&lt;
Znak mniejszości "<"
&gt;
Znak więszkości ">"
&apos;
Apostrof '
&quot;
Cudzysłów "
&DDD;
Znak o kodzie podanym jako liczba dziesiętna DDD
&xHH;
Znak o kodzie podanym jako liczba szesnastkowa HH

Inne, zwykle rzadziej stosowane rodzaje informacji to:

1. <!-- komentarz -->
2. <?adresat informacje ?>
3. <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
4. <![CDATA[ tekst ]]>
  1. Komentarz (pomijany przez programy odczytujące, przeznaczony tylko dla człowieka)
  2. Processing Instruction - informacja przeznaczona dla programu odczytującego dokumemt
  3. Prolog, czyli nagłówek dokumentu XML (tu pokazany przykładowy)
  4. Sekcja CDATA - jej zawartość traktowana jest tak jak zwyczajny tekst, ale może zawierać absolutnie dowolne znaki z wyjątkiem 3-znakowej sekwencji zamykającej

Warto wspomnieć jeszcze o dodatkowych standardach powiązanych z XML:

XPath
Służy do wybierania fragmentów dokumentu XML; stosowany w XSLT
XSLT
Deklaratywny język służący do przetwarzania jednych dokumentów XML na inne (będzie jeszcze wspomniany w rozdziale 7 - "Języki programowania")
Schema
Służy do opisywania strukury dokumentu opartego na XML

Linki

Extensible Markup Language (XML) 1.0 (Third Edition)
Oficjalny opis standardu XML
XML Tutorial
Kurs XML na bardzo przystępnej stronie w3schools.com

EBNF

W dalszej części tego rozdziału utworzymy własny format tekstowy i napiszemy program do jego analizy (parser). Wyobraźmy sobie, że jest to dziennik zdarzeń (log) jakiegoś programu. Przykładowy fragment dokumentu wygląda tak:

[2004-02-22 14:26:25] [notice] Exit event signaled. Child process is ending.
[2004-02-22 14:26:26] [notice] Released the start mutex
[2004-02-22 14:26:27] [notice] Waiting for 250 worker threads to exit.
[2004-02-22 14:26:27] [notice] All worker threads have exited.
[2004-02-22 14:26:27] [notice] Child process is exiting
[2004-02-22 14:26:27] [notice] Child process exited successfully.

W tym miejscu należy się krótka dygresja na temat pewnej klasyfikacji formatów tekstowych:

Z podziałem na wiersze
Każdy wiersz jest pewną jednostką podlegającą osobnej analizie (np. format, który będziemy teraz tworzyli)
Wolny format zapisu
Znak końca wiersza jest traktowany jako jeden z białych znaków i może występować w różnych miejscach nie różniąc się znaczeniem np. od spacji (np. XML)

Natychmiastowe rozpoczęcie implementacji programu analizującego złożony format tekstowy (ten jest w miarę prosty, ale na jego przykładzie pokażę wszystkie istotne rzeczy) byłoby podejściem niemądrym z dwóch powodów:

  1. Łatwo można przeoczyć niektóre możliwe sytuacje i wprowadzić do kodu paskudne błędy ujawniające się tylko wobec pewnych kombinacji znaków.
  2. Pisanie programu analizującego bardziej złożony format tekstowy wydaje się bardzo trudne i przy tym podejściu takie byłoby faktycznie.

BNF (The Backus-Naur Form) to sposób precyzyjnego opisania gramatyki bezkontekstowej jakiegoś języka (opisu lub programowania). EBNF (The Extended Backus-Naur Form) wprowadza do niego elementy składni wyrażeń regularnych, by umożliwić czytelny, a zarazem zwięzły opis. EBNF jest trochę podobny do wyrażeń regularnych. Podstawowa różnica polega na tym, że opis danego formatu składa się z definicji szeregu pojęć, z których jedne odwołują się do innych.

Zanim przejdziemy do przykładów trzeba podkreślić, że EBNF nie jest przeznaczony do analizy przez komputer (choć oczywiście można to zrobić). To tylko precyzyjny opis tworzonego formatu pisany przez nas i dla nas. Jest to doskonały etap pośredni pomiędzy koncepcją tworzonego formatu, a implementacją parsera. Jak się okaże w następnym podrozdziale, dzięki precyzyjnemu opisaniu składni tego formatu pisanie na jego podstawie parsera staje się zadaniem bardzo prostym.

Składnia EBNF została ustandaryzowana. Jednak w praktyce niemal każdy (łącznie z konsorcjum W3C, które stworzyło HTML, XML itp.) do definiowania tworzonych formatów używa własnych konstrukcji. Oto opis naszego formatu dziennika w EBNF:

S = ' '
EOL = "\r\n" | '\n' | '\r'
date_sep = '-'
time_sep = ':'
digit = ['0'-'9']
number = digit+

year = number
month = number
day = number
hour = number
minute = number
second = number

Date = '[' year date_sep month date_sep day S
  hour time_sep minute time_sep second ']'
Type = '[' [^']']* ']'
Text = [^'\r','\n']*

Line = Date S Type (S Text)? EOL?

Możliwe są dwa podejścia. Można albo zaczynać od opisania najprostszych składników (jak liczba, odstęp itp.) i kostruować z nich bardziej złożone, albo zaczynać od opisania całości używjąc elementów prostyszch, które zostaną zdefiniowane dopiero później. Tutaj użyłem pierwszego podejścia. Przeanalizujmy po kolei ten przykład.

Każda definicja składa się z nazwy, znaku równości i opisu. Opis składa się z kolejno następujących po sobie składników. Pośród nich mogą znajdować się elementy zdefiniowane gdzieś indziej.

Najpierw zdefiniowane zostały podstawowe elementy. Spacja, oznaczona krótko jako "S", została opisana za pomocą pojedynczego znaku. Pojedyncze znaki obejmuje się w apostrofy. Koniec wiersza "EOL" to jedna z możliwych sekwencji znaków. Pionowa kreska oznacza alternatywę. Wieloznakowe łańcuchy obejmuje się w cudzysłów.

Do zdzefiniowania cyfry "digit" użyłem konstrukcji znanej z wyrażeń regularnych, która oznacza dowolny znak z podanego zakresu. Podobnie można się domyśleć (znając wyrażenia regularne), że liczba "number" to jedna lub więcej cyfr.

Składniki daty (rok, miesiąc, dzień, godzina, minuta i sekunda) zdefiniowane zostały jako synonimy liczby. Wbrew pozorom taka konstrukcja ma sens. Dzięki niej można będzie np. prosto zmienić definicję miesiąca na jeden z łańcuchów "Jan", "Feb" itd., jeśli w przyszłości zajdzie taka potrzeba.

Dalej znajdują się coraz bardziej złożone i ogólne elementy. Spójrz jeszcze raz na przykładowy fragment dokumentu w naszym formacie. Data składa się kolejno z otwarcia nawiasu kwadratowego, roku, separatora daty, miesiąca itd., aż do zamknięcia nawiasu.

Typ komunikatu zdefiniowałem jako otwarcie nawiasu kwadratowego, zamknięcie nawiasu kwadratowego, a pomiędzy nimi sekwencja dowolnej ilości (mówi o tym gwiazdka) dowolnych znaków z wyjątkiem (ptaszek) zamknięcia nawiasu kwadratowego (znak objęty w apostrofy). Podobnie test do sekwencja dowolnej ilości dowolnych znaków z wyjątkiem znaków końca wiersza.

Wreszcie linia, jako podstawowy i największy rekord danych, składa się ze zdefiniowanych wcześniej części - daty, typu oraz tekstu odddzielonych spacjami. Zwróć uwagę, że obecność ostatniej spacji i tekstu (jako całości - nawiasu służą tylko do grupowania) jest opcjonalna. Co by się stało, gdyby nie była?

Tekst zdefiniowany jest jako dowolna liczba znaków, także zero. Wobec tego, gdyby po typie informacji nastąpiła spacja i od razu koniec wiersza, tekstem byłby po prostu łańcuch pusty. Jednak bez tej spacji nastąpiłby błąd, bo jest wymagana w definicji linii. My zakładamy tutaj, że tekstu nie musi być.

Koniec wiersza też jest opcjonalny. Dzięki temu ostatnia linia w pliku nie musi się nim kończyć. Czy nie spowoduje to jednak pominięcia go wtedy, kiedy wytępuje, np. między wierszami? Nie, ponieważ po to są elementy opcjonalne (znak zapytania oznacza, że może wystąpić 0 lub 1 razy), że o ile tylko występują, są odczytywane.

Na tym kończymy omawianie EBNF. Cały ten standard jest prosty i bardzo użyteczny, a za jego pomocą można opisywać składnię nawet tak złożonych języków, jak języki programowania, np. C++.

Parsery

Najwyższy czas zdefiniować pojęcie, którego używamy od dawna... Każdy format tekstowy zawiera, oprócz istotnych danych, także różne nadmiarowe informacje (jak odstępy i znaki końca wiersza) tworzące jego układ. Same informacje też są odpowiednio zakodowane (wszystkie w postaci tekstowej) i nierzadko umieszczone wewnątrz specjalnych znaków (np. cudzysłowów). To sprawia, że pisząc program musisz uwzględniać składnię danego języka i niejako wyławiać z dokumentu istotne dane.

Znając specyfikację takiego języka można jednak napisać moduł, który zamknie w sobie wszelkie zagadnienia obsługi jego składni. Używając takiego modułu, można otrzymywać z niego tylko istotne informacje i dzięki temu skupić się na ich przetwarzaniu. To jest właśnie parser. Warto wprowadzić podział parserów SAX na:

Czynne
Po uruchomieniu parser sam odczytuje kolejne informacje i wywołuje funkcje zwrotne z używającego go kodu
Bierne
Użytkownik parsera musi sam żądać od niego odczytywania kolejnych informacji

Pisząc parser w języku C++ lepiej moim zdaniem wybrać model bierny, ponieważ w C++ brakuje mechanizmu wskaźników na metody (konkretną metodę konkretnego obiektu - jak zdarzenia w Object Pascalu w Delphi), który umożliwiłby wygodną implementację funkcji zwrotnych (callback) w połączeniu z kodem obiektowym.

Zajmiemy się teraz napisaniem parsera naszego wymyślonego formatu. Będziemy przy tym korzystali z opisu jego składni za pomocą EBNF, który został wprowadzony w poprzednim podrozdziale. Przypominam przykładową linijkę dokumentu w tym formacie:

[2004-02-22 14:26:25] [notice] Exit event signaled. Child process is ending.

Jak działa parser? Jako przykład weźmy odczytywanie pierwszej części tej linijki - daty. Pierwszym znakiem jest zawsze otwierający nawias kwadratowy. Możnaby więc rozpoczynać odczytywanie dopiero od drugiego znaku. To jednak nie jest dobre rozwiązanie, bo pozostawia poza kontrolą poprawność tego pierwszego.

Przykładem drugiego często popełnianego błędu byłoby tutaj poszukiwanie nawiasu zamykającego, a następnie parsowanie zawartego w środku tekstu jako daty. To podejście jest złe, ponieważ składnia zawartości tego nawiasu może dopuszczać (choć w tym przypadku nie dopuszcza) istnienie pewnych elementów, które również zawierałyby taki znak.

Jak w takim razie dokonywać tego parsowania? Wyobraźmy sobie odczytywany dokument oraz indeks (numer znaku), którego wartość pokazuje na bieżący znak tego dokumentu. Parser to zbiór funkcji. Każda odpowiada jednemu elementowi składni formatu zdefiniowanemu w EBNF.

Każda funkcja próbuje odczytywać kolejne podelementy, z których składa się odczytywany element zgodnie z jego definicją. Jeśli się to uda, przesuwa indeks za koniec odczytanego tekstu i zwraca odczytane informacje. Jeśli nie uda się, pozostawia indeks niezmieniony, co pozwala funkcji nadrzędnej spróbować odczytać z tego samego miejsca element innego rodzaju.

Poniżej prezentuję kompletny, działający przykład parsera odczytującego nasz format. Zachęcam do jego analizy. Szczególną uwagę zwróć na to, w jaki sposób definicje poszczególnych elementów składniowych zapisane wcześniej w EBNF stały się kodem kolejnych funkcji.

#include <iostream>
#include <sstream>
#include <pxcommon.h>

std::string doc;

bool Parse_Char(size_t* index, char ch)
{
  if (*index < doc.size() && doc[*index] == ch) {
    (*index)++;
    return true;
  }
  else return false;
}

bool Parse_S(size_t* index)
{
  return Parse_Char(index, ' ');
}

bool Parse_EOL(size_t* index)
{
  if (*index < doc.size()) {
    if (doc[*index] == '\n') {
      (*index)++;
      return true;
    }
    else if (doc[*index] == '\r') {
      (*index)++;
      if (*index < doc.size() && doc[*index] == '\n')
        (*index)++;
      return true;
    }
    else return false;
  }
  else return false;
}

bool Parse_DateSep(size_t* index) { return Parse_Char(index, '-'); }
bool Parse_TimeSep(size_t* index) { return Parse_Char(index, ':'); }

bool Parse_Digit(size_t* index, char* c)
{
  if (*index < doc.size() && doc[*index] >= '0' && doc[*index] <= '9') {
    *c = doc[*index];
    (*index)++;
    return true;
  }
  else return false;
}

bool Parse_Number(size_t* index, int* number)
{
  char c;
  std::string s;
  size_t i = *index;
  while (Parse_Digit(&i, &c))
    s += c;
  if (s.empty())
    return false;
  else {
    *number = px::str2int(s);
    *index = i;
    return true;
  }
}

inline bool Parse_Year(size_t* index, int* year)
{ return Parse_Number(index, year); }
inline bool Parse_Month(size_t* index, int* month)
{ return Parse_Number(index, month); }
inline bool Parse_Day(size_t* index, int* day)
{ return Parse_Number(index, day); }
inline bool Parse_Hour(size_t* index, int* hour)
{ return Parse_Number(index, hour); }
inline bool Parse_Minute(size_t* index, int* minute)
{ return Parse_Number(index, minute); }
inline bool Parse_Second(size_t* index, int* second)
{ return Parse_Number(index, second); }

bool Parse_Date(size_t* index, std::string* date)
{
  int year, month, day, hour, minute, second;
  size_t i = *index;
  date->clear();
  if (!Parse_Char(&i, '[')) return false;
  if (!Parse_Year(&i, &year)) return false;
  if (!Parse_DateSep(&i)) return false;
  if (!Parse_Month(&i, &month)) return false;
  if (!Parse_DateSep(&i)) return false;
  if (!Parse_Day(&i, &day)) return false;
  if (!Parse_S(&i)) return false;
  if (!Parse_Hour(&i, &hour)) return false;
  if (!Parse_TimeSep(&i)) return false;
  if (!Parse_Minute(&i, &minute)) return false;
  if (!Parse_TimeSep(&i)) return false;
  if (!Parse_Second(&i, &second)) return false;
  if (!Parse_Char(&i, ']')) return false;
  *index = i;
  std::ostringstream os;
  os << day << '.' << month << '.' << year << ' ' << hour << ':' <<
    minute << ':' << second;
  *date = os.str();
  return true;
}

bool Parse_Type(size_t* index, std::string* date)
{
  size_t i = *index;
  date->clear();
  if (!Parse_Char(&i, '[')) return false;
  while (i < doc.size() && doc[i] != ']')
    *date += doc[i++];
  if (!Parse_Char(&i, ']')) return false;
  *index = i;
  return true;
}

bool Parse_Text(size_t* index, std::string* text)
{
  text->clear();
  while (*index < doc.size() && doc[*index] != '\r' &&
 doc[*index] != '\n')
    *text += doc[(*index)++];
  return true;
}

bool Parse_Line(size_t* index, std::string* date, std::string* type,
        std::string* text)
{
  size_t i = *index;
  if (!Parse_Date(&i, date)) return false;
  if (!Parse_S(&i)) return false;
  if (!Parse_Type(&i, type)) return false;
  if (Parse_S(&i)) {
    if (!Parse_Text(&i, text)) return false;
  }
  Parse_EOL(&i);
  *index = i;
  return true;
}

int main()
{
  std::string date, type, text;
  size_t index = 0, line = 1;
  if (px::LoadStringFromFile("doc.txt", doc)) {
    while (Parse_Line(&index, &date, &type, &text)) {
      std::cout << "Linia:\n  data='" << date << "' typ='" << type <<
        "'\n  tekst='" << text << "'\n";
      line++;
    }
    if (index < doc.size())
      std::cout << "Blad: parsowanie przerwane na wierszu " <<
      static_cast<unsigned>(line) << std::endl;
  }
  else
    std::cout << "Blad: nie mozna wczytac pliku" << std::endl;
}

Pisanie takiego parsera, mając przed oczami precyzyjny opis formatu w EBNF, jest naprawdę bardzo proste, niemal mechaniczne, a po napisaniu taki parser od razu działa poprawnie! Wynik działania tego programu dla przedstawionego w poprzednim podrozdziale przykładowego dokumentu wygląda tak:

Linia:
  data='22.2.2004 14:26:25' typ='notice'
  tekst='Exit event signaled. Child process is ending.'
Linia:
  data='22.2.2004 14:26:26' typ='notice'
  tekst='Released the start mutex'
Linia:
  data='22.2.2004 14:26:27' typ='notice'
  tekst='Waiting for 250 worker threads to exit.'
Linia:
  data='22.2.2004 14:26:27' typ='notice'
  tekst='All worker threads have exited.'
Linia:
  data='22.2.2004 14:26:27' typ='notice'
  tekst='Child process is exiting'
Linia:
  data='22.2.2004 14:26:27' typ='notice'
  tekst='Child process exited successfully.'

Omówienia wymaga jeszcze pojęcie tzw. zachłanności. W naszym przykładzie treść typu zdefiniowana została jako ciąg dowolnych znaków z wyjątkiem znaku oczekiwanego jako jej zakończenie (zamknięcie nawiasu kwadratowego). Dzięki temu można było odczytywać znaki zachłannie - tak dużo, jak to możliwe.

Można zastosować inne podejście. Treść typu można zdefiniować jako ciąg absolutnie dowolnych znaków. Z każdym kolejnym odczytywanym znakiem najpierw sprawdzać, czy nie następuje zakończenie (zakończeniem nie musi być jeden znak - może być cała sekwencja, którą próbuje odczytać osobna funkcja), a dopiero w przeciwnym wypadku traktować go jako dalszy ciąg łańcucha. Jak widać, zagadnienie zachłanności sprowadza się więc do kolejności sprawdzania różnych możliwych elementów w danym miejscu.

Na zakończenie tego rozdziału wypada jeszcze napisać kilka słów o decyzjach, jakie stoją przed twórcą formatu i autorem parsera. Mówiąc ogólnie, trzeba znaleźć rozsądny kompromis pomiędzy kontrolą i sygnalizowaniem błędów, a wydajnością (szybkością) parsera. Należy przy tym uwzględnić przewidywane zastosowanie (czy wydajność jest tam sprawą kluczową?).

Drugim problemem jest decyzja w sprawie traktowania błędów składniowych. Możliwe są tutaj dwa podejścia:

Zgłaszać błąd
Napotkanie pierwszego błędu powoduje zaprzestanie dalszego parsowania. Należy zgłosić błąd podając jego rodzaj i miejsce wystąpienia (wiersz, kolumna, numer znaku oraz fragment w okolicy tego miejsca). W ten sposób reagują np. parsery XML.
Parsować dalej
Parser próbuje poradzić sobie z błędem przeskakując do jakiegoś dalszego miejsca i parsować dalej,
  • zgłaszając ewentualnie kolejne błędy (np. kompilatory C++ i nowszych wersje Delphi) lub
  • po prostu przetwarzając wszystko, najwyżej niezgodnie z oczekiwaniami (np. przeglądarki WWW parsujące HTML).

Wbrew pozorom, każde z tych rozwiązań ma sens w niektórych zastosowaniach. XML musi być zawsze doskonale poprawny, bo takie jest jego założenie. Kontynuacja parsowania podczas kompilacji pozwala kompilatorowi odnaleźć, a programiście poprawić wiele błędów na raz. Strona HTML pokaże się i prawdopodobnie pozostanie czytelna mimo ewentualnych błędów składniowych.

Trzecim problemem jest sprawdzanie zakresu dozwolonych znaków. Pojawia się pytanie, jakie znaki dopuszczać w treści pewnych nazw, tytułów itp.?

Tylko z dozwolonego zakresu
Lepsza kontrola błędów, ale zarazem pewne ograniczenia (co z literami polskimi czy japońskimi?) i więcej pracy ze sprawdzaniem każdego znaku
Dowolne z wyjątkiem tych oczekiwanych jako zakończenie
Gorsza kontrola błędów, ale większa elastyczność i szybsze parsowanie

Języki programowania

Pojęcie języka programowania z pewnością jest ci (jako czytelnikowi, który dotrwał aż tutaj :) znane, dlatego nie będziemy go definiowali. Musimy jednak opisać kilka spraw związanych z ich strukturą oraz wprowadzić pewien podział.

O językach programowania

Języki programowania dzielą się na generacje:

I generacja - kod maszynowy
Polecenia procesora najniższego poziomu
II generacja - asemblery
Bezpośrednia, tekstowa reprezentacja rozkazów maszynowych
III generacja - języki symboliczne
Dzielą się na:
Języki liniowe
Program to płaska lista poleceń i instrukcji skoku (np. Fortran, Basic w dawnych wersjach)
Języki skrukturalne
Program składa się z opisu danych i funkcji, które na nich operują (np. C, Pascal w dawnych wersjach)
Języki obiektowe
Program składa się z klas definiujących pola i metody i reprezentujących określone pojęcia (np. C++, Object Pascal)
IV generacja - języki idealnie nieproceduralne (np. SQL)
Nowe metody programowania, specjalizowane do określonych zastosowań

Z punktu widzenia budowy języka programowania istotne są dwa pojęcia i ich rozróżnienie. Pierwszym są instrukcje. Instrukcja to składnik ciała funkcji o określonej budowie przeznaczony do wykonania. Instrukcje wykonywane są po kolei, ale mogą zawierać wewnątrz inne instrukcje, a także zmieniać przebieg sterowania (pewne instrukcje pominać lub powtarzać) itp. Rozważmy przykład kodu w C++:

void MojaFunkcja()
{
  if (ziemia_jest_plaska == true) {
    uwazaj_zeby_nie_spasc();
  }
  while (!blad()) {
    zajmuj_pamiec();
    if (rand()%10 == 0)
      spowoduj_blad();
  }
}

Najpierw wykona się pierwsza instrukcja warunkowa, a następnie pętla.

Drugim ważnym pojęciem jest wyrażenie. Wyrażenie posiada (zwraca) wartość jakiegoś typu. Może być zagnieżdżane wewnątrz innego wyrażenia, a także w pewnych przewidzianych miejscach w instrukcji, np.:

if ( 2*2 == sqrt(16) ) {
  std::cout << "Ta matematyka naprawdę działa :)" << std::endl;
}

Mnożenie dwóch liczb całkowitych jest wyrażeniem, które daje w wyniku również liczbę całkowitą. Wywołanie funkcji jest wyrażeniem, które zwraca taką wartość i takiego typu, jak zwróciła ta funkcja. Wreszcie porównanie jest wyrażeniem, które zwraca wartość logiczną (true lub false).

W szerszym kontekście zwykła stała dosłowna 2 także jest wyrażeniem, bo posiada wartość jakiegoś typu. Każde wyrażenie jest instrukcją - użyte w takiej roli pozostawia swoją wartość, zwróconą po jego wykonaniu jako całości, niewykorzystaną.

Języki programowania można też podzielić na:

Kompilowane
Kod źródłowy jest kompilowany do kodu maszynowego wykonywanego bezpośrednio jako niezależny program (plik EXE)
Interpretowane
Kod źródłowy jest wykonywany przez interpreter

Jednak obecnie ten podział jest coraz mniej sensowny. Nawet instrukcje maszynowe są w niektórych procesorach tłumaczone na wewnętrzny mikrokod i dopiero wykonywane. Z drugiej strony prawie nigdy nie zdarza się, by kod źródłowy jako tekst interpretowany był bezpośrednio. Zawsze najpierw jest rozkładany, przynajmniej na drzewo składniowe i nierzadko tłumaczony (kompilowany?) do postaci pewnego kodu pośredniego (bytecode). Mówi się wtedy czasem o półkompilacji, a zamiast interpretowania używa się terminu: wykonywanie w wirtualnej maszynie. Jak widać więc, to wszystko jest względne.

Języki można podzielić jeszcze inaczej na:

Imperatywne (sterowane kodem)
Wykonywanie kolejnych instrukcji kodu powoduje sterowanie przepływem danych (wszystkie tradycyjne języki programowania)
Deklaratywne (sterowane danymi)
Przychodzące dane powodują wywoływanie pewnych elementów kodu (np. XSLT)

Czasami problemem jest dokonanie wyboru pomiędzy programowaniem, a opisywaniem. Jako przykład wyobraźmy sobie grę, która potrzebuje zapisanych w pliku definicji różnych potworów. Obok koloru, nazwy czy siły każdego z nich potrzeba zdefiniować jego zachowanie. Jak je zapisywać?

  1. Najprostszym podejściem jest wpisanie kilku możliwych zachowań do kodu samej gry, z ewentualnymi parametrami (np. agresywność) i zapisywanie z każdym potworem, którego rodzaju zachowania ma używać.
  2. Rozwiązaniem pośrednim pomiędzy opisywaniem a programowaniem jest pojęcie zapadek (ang. "triggers"). Każda zapadka ma określone zdarzenie, które powoduje jej wywołanie (wraz z ewentualnymi warunkami) oraz listę kolejnych akcji do wykonania. Przykładowo mechanizm zapadek użyty został w edytorach map do gier Warcraft i StarCraft firmy Blizzard.
  3. Najbardziej złożone i elastyczne, ale nie zawsze potrzebne rozwiązanie to zastosowanie najprawdziwszego języka programowania - tzw. języka skryptowego, którego kod interpretowany wewnątrz gry będzie wchodził w interakcję z wirtualnym światem, np. sterując zachowaniem danego potwora.

Linki

Oto języki skryptowe, które można osadzać w swoich programach:

LUA
Bardzo mały, prosty, szybki; przeznaczony specjalnie do osadzania; wyróżnia się możliwością rozbudowy jego składni o nowe, potrzebne elementy
Python
Bardzo rozbudowany i potężny język o ciekawej składni
TCL
Bardzo popularny język; ma nieprzyjemną składnię

Wyrażenia matematyczne

Po tych wiadomościach wstępnych dotyczących języków programowania rozpoczynamy omawianie metod ich parsowania. Ponieważ jednak ten temat jest tak trudny i obszerny, że zasługuje na osobny artykuł (lub nawet książkę), ograniczę się w tym rozdziale do ogólnego omówienia podstawowych zagadnień.

Czasami wystarczy zapewnić ewaluację (czyli obliczanie wartości) wyrażeń, bez pełnej składni języków programowania. Najprostszym przykładem może być program do rysowania wykresów funkcji. Za pomocą samych wyrażeń można zdziałać bardzo wiele. Przykładowo warunki można zaimplementować w postaci wyrażenia złożonego z 3 podwyrażeń, z których zwracana jest wartość drugiego, jeśli wartością pierwszego jest prawda i wartość trzeciego, jeśli wartością pierwszego jest fałsz, np.:

x + 2 + if( x > 3, 10, -10 )

Podczas parsowania wyrażeń matematycznych pojawia się problem priorytetu operatorów, znany ze szkoły jako kolejność wykonywania działań. Polega on na właściwym ustawieniu ważności poszczególnych podwyrażeń w przypadku, kiedy kolejność ich obliczania nie jest wymuszona za pomocą nawiasów, np.:

2+2*2   to   2+(2*2)

Innym problemem jest łączność operatorów. Określa ona kolejność obliczania podwyrażeń równorzędnych od lewej lub od prawej strony. Podczas ewaluacji zwykłych wyrażeń matematycznych łączność jest zawsze lewostronna, ale nie zawsze musi tak być. Np. przypisanie (które w C++ też jest wyrażeniem, a zwraca wartość przypisywaną) jest łączne prawostronnie. Oto przykłady:

2+3+4+5   to   ((2+3)+4)+5
x=y=z=0   to   x=(y=(z=0))

Przed obliczeniem wyrażenie można zamienić na drzewo. Takie przedstawienie umożliwia wiele ciekawych rzeczy, np. obliczanie pochodnych.

2+2*2

   +
 /   \
2     *
    /   \
   2     2

Takiej zamiany, wraz z poprawnym rozpoznaniem priorytetu operatorów, może dokonać zwyczajny parser napisany na podstawie odpowiednio ułożonego opisu w EBNF, np.:

wyrażenie = (wyrażenie '+' składnik) | (wyrażenie '-' składnik)
 | składnik
składnik = (składnik '*' czynnik) | (składnik '/' czynnik) | czynnik
czynnik = ('(' wyrażenie ')') | ('-' czynnik) | liczba | zmienna

Pewne znaczenie podczas parsowania i ewaluacji wyrażeń ma odwrotna notacja polska (ang. "Reverse Polish Notation", w skrócie ONP lub RPN). Jest ona sposobem zapisywania wyrażeń, w którym symbol wykonywanej operacji umieszczony jest po operandach (a nie pomiędzy nimi jak w konwencjonalnym zapisie). Zapis ten pozwala na całkowitą rezygnację z użycia nawiasów w wyrażeniach, jako że jednoznacznie określa kolejność wykonywanych działań. Przykład:

Normalnie : (2+3)*5
ONP       : 2 3 + 5 *

Jest to dobry sposób płaskiej reprezentacji przedstawionego wyżej drzewa. Algorytmy dokonując analizy wyrażeń często przekształcają je na odwrotną notację polską. Bardzo ułatwia ona wykonywanie obliczeń z nawiasami i zachowaniem kolejności działań. Zarówno algorytm konwersji notacji konwencjonalnej (infiksowej) na odwrotną notację polską (postfiksową), jak i algorytm obliczania wartości wyrażenia danego w ONP są bardzo proste i wykorzystują stos.

Kod

W przeciwieństwie do języków opisu, kod języków programowania nie jest parsowany bezpośrednio z łańcucha. Najpierw dokonywana jest tokenizacja, czyli zamiana na ciąg tokenów. Oto przykłady tokenów różnego rodzaju (każdy token składa się z rodzaju i zależnej od niego treści):

identyfikatory   x
                 moja_funkcja
operatory        +
                 !=
liczby           9
                 314.15e-2
łańcuchy         "ble"
                 "\r\n"

Dopiero taki ciąg tokenów jest rozkładany na drzewo. Następnie takie drzewo jest na różne sposoby sprawdzane, np. pod kątem zgodności typów. Wreszcie może zostać skompilowane do jakiegoś płaskiego kodu, który będzie wykonywany (np. do kodu maszynowego). Można też wyobrazić sobie wykonywanie bezpośrednio tego drzewa, bez jego spłaszczania.

Program przetwarzający kod musi dokonywać optymalizacji. Niemądrze byłoby pozostawiać wyrażenie 2+2 do każdorazowego obliczania podczas wykonywania zamiast zastąpić go pojedynczą wartością 4, skoro da się ją wyznaczyć już na etapie kompilacji. Warto usuwać nieużywane zmienne, funkcje, moduły... Łatwo można sobie wyobrazić wiele różnych optymalizacji.

Linki

The Lex & Yacc Page
Narzędzie to automatyzacji tworzenia parserów
Kompilatory. Reguły, metody i narzędzia
Książka o pisaniu kompilatorów; autorzy: Aho Alfred V., Sethi Ravi, Ullman Jeffrey D.; wydawnictwo: WNT

Ciekawoski

Na koniec rozdziału o językach programowania opiszę kilka ciekawostek, które są raczej mało znane, choć z punktu widzenia tematyki tego artykułu z pewnością godne uwagi.

Języki ezoteryczne

Istnieje bardzo wiele języków programowania. Jedne są uniwersalne, inne specjalistyczne. Jedne stare i już dawno zapomniane, inne cieszą się niesłabnącą popularnością, jeszcze inne przeżywają swój chwilowy rozkwit czy też dopiero czekają na swój czas.

Są też języki stworzone ot tak, dla zabawy. Ich autorzy zakładają (całkiem słusznie), że dla wielu pasjonatów programowanie jest dobrą zabawą i wymyślają najróżniejsze, nietypowe sposoby na programowanie. Angielskie "esoteric" znaczy "ezoteryczny", ale też "poufny", "wtajemniczony". Języki ezoteryczne są więc adresowane tylko do prawdziwych maniaków programowania. Oto niektóre z nich:

Brainfuck
Język absolutnie minimalistyczny, podobny trochę do maszyny Turinga. Składa się tylko z 8 poleceń. Oto program wypisujący "Hello world":

>+++++++++[<++++++++>-]<.>+++++++[<++++>-]<+.+++++++..+++.[-]
>++++++++[<++++>-]
<.#>+++++++++++[<+++++>-]<.>++++++++[<+++>-]<.+++.------.--------.
[-]>++++++++[
<++++>-]<+.[-]++++++++++.

Malbolge i Dis
Język stworzony z założeniem, że programowanie powinno być jak najtrudniejsze. Oto program wypisujący trzy szóstki:

*>>*|{{{!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!{**

Funge
W językach z tej rodziny (Befunge, Unefunge, Trifunge) kod składa się nie z listy rozkazów, ale z tablicy rozkazów (dwu- lub nawet trójwymiarowej). Oto program wypisujący "Hello world!":
                 v
>v"Hello world!"0<
,:
^_25*,@

Zaciemniony kod

Jedyny słuszny sposób pisania kodu źródłowego, którego uczymy się dążąc do doskonałości, nie jest jedynym możliwym. Niektórzy lubią wyczyniać najróżniejsze rzeczy nie tylko z treścią, ale i z formą swojego kodu.

Poniższa funkcja pochodzi ze wstępu do książki pt. "Perełki programowania gier, tom 3". Po dołączeniu biblioteki "stdio.h" rysuje (oczywiście tekstowo, na konsoli :) znany fraktal - zbiór Mandelbrotta. Takie krótkie kody nadają się też jako sygnatury do listów elektronicznych.

f(){int k;float i,j,r,x,y=-16;while(puts(""),y++<15)for(x
=0;x++<79;putchar(" .:-;!/>)|&IH%*#"[k&15]))for(i=k=r=0;
j=r*r-i*i-2+x/25,i=2*r*i+y/10,j*j+i*i<11&&k++<111;r=j);}
The International Obfuscated C Code Contest
Konkurs na najbardziej zaciemniony kod

Wojny rdzeniowe

Wojny rdzeniowe (ang. "CoreWars") to stara zabawa programistów. Programy napisane w specjalnym języku podobnym do Assemblera (tzw. "RedCode") są równolegle wykonywane w wirtualnej maszynie. Współdzielą one jeden obszar pamięci i jedyne, co mogą robić, to operować na tej pamięci. Zadaniem programu jest takie jej zmodyfikowanie, aby kod przeciwnika wykonał niedozwoloną operację. Oto prosty program do wojen rdzeniowych znany jako "Dwarf":

ADD #4, 3
MOV 2, @2
JMP -2
DAT #0, #0

Zakończenie

Wszędzie wokół nas coraz więcej jest multimediów: grafiki, dźwięku, video. Tymczasem tekst jest i na zawsze pozostanie istotnym, a w wielu miejscach najważniejszym czy najlepszym sposobem komunikacji, przechowywania i przesyłania informacji, a także programowania komputera.

Niektóre zaprezentowane tutaj tematy są, jak to się mówi, niszowe. Znanie niewielu ludziom, opisywane w Internecie w pojedynczych, niedbałych dokumentach HTML, pozostają poza kręgiem zainteresowania większości ludzi i dużych, "poważnych" serwisów. Niech tak pozostanie. To naturalne, że tekst jest mało atrakcyjny.

Możliwe więc, że ASCII Art, gry tekstowe czy udziwnione języki programowania nie wzbudziły w tobie większego zainteresowania. Mam jednak nadzieję, że przynajmniej dostrzegasz istotną rolę, jaką tekst i jego przetwarzanie odgrywa w komputerze.

Ten artykuł stanowi przegląd całej mojej wiedzy, jaką zebrałem podczas wielu lat mojego zainteresowania wszystkim, co tekstowe. Jednak nikt nie może powiedzieć, że wszystko, co zrobił, osiągnął wyłącznie dzięki sobie. Pragnę więc podziękować kilku osobom. Jako pierwszy, na wspomnienie zasługuje g[R]eK. To on nauczył mnie tej trudnej sztuki parsowania. Dziękuję mu za nasze długie rozmowy na "priv" oraz za całą naszą wymianę wiedzy i poglądów.

Na podziękowania zasługuje również Xion, który niestrudzenie pisząc swój megatutorial daje dobry przykład i pokazuje swoim postępowaniem, że warto dzielić się z innymi swoją wiedzą, a nie tylko ją wykorzystywać. Pozdrowienia otrzymuje też gemGreg, DexteR (AyuFan) i wszyscy fajni ludzie z Warsztatu. To wszystko dzięki wam i dla was! :)

Na zakończenie pokażę jeszcze jedną ciekawostkę. Jeśli jesteś prawdziwym maniakiem komputerów, możesz stworzyć własną sygnaturę w specjalnym kodzie, która będzie opisywała twoją osobę. Ja swojej jeszcze nie stworzyłem...

The Geek Code

Adam Sawicki "Regedit"
2004-02-29
www.programex.prv.pl
www.regedit.risp.pl

 

Spis treści Redakcja @t Newsy
Software Hardware Internet Webmastering System Programowanie Grafika Telefonia Film Gry Magazyn Humor

Spis treści